@apocaliss92/scrypted-advanced-notifier 4.8.37 → 4.8.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/plugin.zip +0 -0
  3. package/docs/INTERFACE_DESCRIPTORS_MIGRATION.md +184 -0
  4. package/docs/PLAN-RECORDER-MIXIN-AND-UNIFIED-PIPELINE.md +203 -0
  5. package/fs-camstack/_expo/static/css/styles-c5d89ce0dbf15ca8fde5bf5e6193a6f9.css +1 -0
  6. package/fs-camstack/_expo/static/css/styles-d46d3dc27ce9bd30adff69475412fa30.css +1 -0
  7. package/fs-camstack/_expo/static/js/web/entry-b6b6b453ef5a7ad0ab32257e831f1ffe.js +2143 -0
  8. package/fs-camstack/_expo/static/js/web/entry-cedcf6140f53df1f869a8bc88b31ebad.js +2148 -0
  9. package/fs-camstack/assets/assets/icons/advanced-notifier.95010eb260a0f87cc521de7f04124521.png +0 -0
  10. package/fs-camstack/assets/assets/icons/frigate.55caee51902df1bfe5039d2893492cfa.png +0 -0
  11. package/fs-camstack/assets/assets/icons/homeassistant.37cdc13336b92f8e1a011ade30d70a6e.png +0 -0
  12. package/fs-camstack/assets/assets/icons/scrypted.ace86857c20ef58731965079ad1661c4.png +0 -0
  13. package/fs-camstack/favicon.ico +0 -0
  14. package/fs-camstack/favicon.png +0 -0
  15. package/fs-camstack/icon-128.png +0 -0
  16. package/fs-camstack/icon-144.png +0 -0
  17. package/fs-camstack/icon-152.png +0 -0
  18. package/fs-camstack/icon-192.png +0 -0
  19. package/fs-camstack/icon-384.png +0 -0
  20. package/fs-camstack/icon-512-maskable.png +0 -0
  21. package/fs-camstack/icon-512.png +0 -0
  22. package/fs-camstack/icon-72.png +0 -0
  23. package/fs-camstack/icon-96.png +0 -0
  24. package/fs-camstack/index.html +39 -0
  25. package/fs-camstack/manifest.json +19 -0
  26. package/fs-camstack/metadata.json +1 -0
  27. package/package.json +26 -3
package/CHANGELOG.md CHANGED
@@ -1,6 +1,10 @@
1
1
  <details>
2
2
  <summary>Changelog</summary>
3
3
 
4
+ ### 4.8.39
5
+
6
+ Added section on every battery camera to setup the battery management based on customizable thresholds
7
+
4
8
  ### 4.8.37
5
9
 
6
10
  - **On-generated sequences**: new sequence hook for Detection, Occupancy and Timelapse rules. Runs when all rule artifacts (video, gif, image) have been generated. Configurable per rule under "On-generated sequences". Script actions in the sequence receive a `payload` object (e.g. `variables.payload` in Scrypted scripts) with `rule`, `videoUrl`, `gifUrl`, `imageUrl` (and for timelapse also `videoPath`, `imagePath`). Any sequence can now receive an optional payload and forward it to linked scripts.
package/dist/plugin.zip CHANGED
Binary file
@@ -0,0 +1,184 @@
1
+ # Migrazione da Webhook HTTP a Socket SDK con interfaceDescriptors
2
+
3
+ Piano per esporre i metodi Events App via **interfaceDescriptors** (come [@scrypted/llm](https://github.com/scryptedapp/llm)), eliminando le chiamate REST e usando solo la socket SDK.
4
+
5
+ ---
6
+
7
+ ## 1. Come funziona interfaceDescriptors (LLM plugin)
8
+
9
+ Dal [package.json dell'LLM](https://github.com/scryptedapp/llm/blob/main/package.json):
10
+
11
+ ```json
12
+ {
13
+ "scrypted": {
14
+ "interfaces": ["DeviceProvider", "UserDatabase", ...],
15
+ "interfaceDescriptors": {
16
+ "UserDatabase": {
17
+ "name": "UserDatabase",
18
+ "methods": ["openDatabase"],
19
+ "properties": []
20
+ }
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ - **interfaceDescriptors** dichiara interfacce custom con metodi e proprietà
27
+ - Il server Scrypted usa questi descrittori per esporre i metodi via RPC sulla socket
28
+ - Il client può chiamare `device.openDatabase()` invece di fare HTTP
29
+
30
+ ---
31
+
32
+ ## 2. Metodi Events App da esporre (handleEventsAppRequest)
33
+
34
+ | apimethod | payload | Note |
35
+ |-----------|---------|------|
36
+ | GetConfigs | — | |
37
+ | GetCamerasStatus | — | |
38
+ | GetEvents | fromDate, tillDate, limit, offset, sources, cameras, detectionClasses, eventSource, filter, groupingRange | |
39
+ | GetVideoclips | fromDate, tillDate, limit, offset, cameras, detectionClasses | |
40
+ | GetCameraDayData | deviceId, day | |
41
+ | GetClusteredDayData | deviceId, days, bucketMs, enabledClasses, classFilter | |
42
+ | GetClusterEvents | clusterId, deviceId, startMs, endMs | |
43
+ | GetArtifacts | deviceId, day | |
44
+ | GetLatestRuleArtifacts | deviceId, limit | |
45
+ | RemoteLog | level, message | |
46
+
47
+ ---
48
+
49
+ ## 3. Modifiche al plugin advanced-notifier
50
+
51
+ ### 3.1 package.json — aggiungere interfaceDescriptors
52
+
53
+ ```json
54
+ {
55
+ "scrypted": {
56
+ "interfaces": ["Settings", "DeviceProvider", "MixinProvider", "HttpRequestHandler", "Videoclips", "LauncherApplication", "PushHandler"],
57
+ "interfaceDescriptors": {
58
+ "EventsAppApi": {
59
+ "name": "EventsAppApi",
60
+ "methods": [
61
+ "getConfigs",
62
+ "getCamerasStatus",
63
+ "getEvents",
64
+ "getVideoclips",
65
+ "getCameraDayData",
66
+ "getClusteredDayData",
67
+ "getClusterEvents",
68
+ "getArtifacts",
69
+ "getLatestRuleArtifacts",
70
+ "remoteLog"
71
+ ],
72
+ "properties": []
73
+ }
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ ### 3.2 utils.ts — costante interfaccia
80
+
81
+ ```ts
82
+ export const EVENTS_APP_API_INTERFACE = "EventsAppApi";
83
+ ```
84
+
85
+ ### 3.3 main.ts — aggiungere interfaccia al data fetcher
86
+
87
+ In `onDeviceDiscovered` per DATA_FETCHER_NATIVE_ID:
88
+
89
+ ```ts
90
+ interfaces: [
91
+ ScryptedInterface.VideoClips,
92
+ ScryptedInterface.EventRecorder,
93
+ ScryptedInterface.Settings,
94
+ EVENTS_APP_API_INTERFACE, // <-- aggiungere
95
+ ],
96
+ ```
97
+
98
+ ### 3.4 dataFetcher.ts — implementare EventsAppApi
99
+
100
+ La classe `AdvancedNotifierDataFetcher` deve implementare i metodi pubblici che mappano 1:1 con gli apimethod. Esempio:
101
+
102
+ ```ts
103
+ // EventsAppApi interface
104
+ async getConfigs(): Promise<{ cameras: ...; enabledDetectionSources: string[] }> {
105
+ const { statusCode, body } = await this.handleEventsAppRequest('GetConfigs', {});
106
+ if (statusCode !== 200) throw new Error(JSON.stringify(body));
107
+ return body as any;
108
+ }
109
+ async getCamerasStatus(): Promise<CamerasStatusResponse> { ... }
110
+ async getEvents(payload: GetEventsPayload): Promise<GetEventsResponse> { ... }
111
+ // ... etc
112
+ ```
113
+
114
+ Oppure, più pulito: estrarre la logica da `handleEventsAppRequest` in metodi dedicati e far sì che `handleEventsAppRequest` li chiami, così si evita duplicazione.
115
+
116
+ ---
117
+
118
+ ## 4. Come il server Scrypted gestisce interfaceDescriptors
119
+
120
+ Il server Scrypted (koush/scrypted) legge `interfaceDescriptors` dal `package.json` del plugin. Quando un device dichiara un'interfaccia in `interfaces`, il server:
121
+
122
+ 1. Verifica che l'interfaccia sia in `interfaceDescriptors` (per interfacce custom)
123
+ 2. Espone i metodi via RPC sulla socket Engine.IO
124
+ 3. Il client `@scrypted/client` può chiamare `device.getConfigs()` e la chiamata viene serializzata e inviata via socket
125
+
126
+ Non serve modificare il server: il supporto è già presente. Il client deve solo usare `client.systemManager.getDeviceById(deviceId)` e chiamare i metodi sull'oggetto restituito.
127
+
128
+ ---
129
+
130
+ ## 5. Modifiche al client (camstack / scrypted-an-frontend)
131
+
132
+ ### 5.1 Trovare il device Events App
133
+
134
+ Il device "Advanced notifier data fetcher" ha tipo `API` e implementa `EventsAppApi`. Per ottenere il suo ID:
135
+
136
+ ```ts
137
+ const state = client.systemManager.getSystemState();
138
+ const eventsAppDeviceId = Object.entries(state).find(
139
+ ([_, d]) => (d as any)?.interfaces?.includes?.('EventsAppApi')
140
+ )?.[0];
141
+ ```
142
+
143
+ Oppure cercare per nome/tipo se lo stato lo espone.
144
+
145
+ ### 5.2 Sostituire fetch con chiamate SDK
146
+
147
+ **Prima (HTTP):**
148
+ ```ts
149
+ const res = await fetch(`${baseUrl}/eventsApp`, {
150
+ method: 'POST',
151
+ body: JSON.stringify({ apimethod: 'GetClusteredDayData', payload: { deviceId, days, bucketMs } }),
152
+ headers: { 'Content-Type': 'application/json', Authorization: getAuthHeader(auth) },
153
+ });
154
+ const data = await res.json();
155
+ ```
156
+
157
+ **Dopo (Socket):**
158
+ ```ts
159
+ const client = await getScryptedClient(auth);
160
+ const device = client.systemManager.getDeviceById(eventsAppDeviceId) as EventsAppApi;
161
+ const data = await device.getClusteredDayData({ deviceId, days, bucketMs });
162
+ ```
163
+
164
+ ### 5.3 Cosa resta su HTTP
165
+
166
+ - **URL di immagini/thumbnail/video**: usati in `<Image src={url} />` e `<Video source={{ uri }} />` — devono restare URL HTTP. Il plugin continua a servire `/eventThumbnail/...`, `/eventImage/...`, `/eventVideoclip/...` via HttpRequestHandler.
167
+ - **Autenticazione**: la socket SDK usa già le credenziali del client (login con username/password). Non serve più Basic auth per le chiamate dati.
168
+
169
+ ---
170
+
171
+ ## 6. Ordine di implementazione
172
+
173
+ 1. **Plugin**: aggiungere `interfaceDescriptors` e `EVENTS_APP_API_INTERFACE`, implementare i metodi su `AdvancedNotifierDataFetcher`
174
+ 2. **Mantenere HttpRequestHandler**: per `apimethod` POST a `/eventsApp` — opzionale durante la transizione (fallback)
175
+ 3. **Client**: creare `eventsAppSdk.ts` che usa la socket; `eventsAppApi.ts` può passare a usare l'SDK quando il client è connesso
176
+ 4. **Rimuovere** le chiamate fetch a `/eventsApp` dal client una volta validato l'SDK
177
+
178
+ ---
179
+
180
+ ## 7. Riferimenti
181
+
182
+ - [LLM plugin package.json](https://github.com/scryptedapp/llm/blob/main/package.json) — esempio interfaceDescriptors
183
+ - [Scrypted Developer Docs](https://developer.scrypted.app/) — interfacce e plugin
184
+ - [@scrypted/client](https://www.npmjs.com/package/@scrypted/client) — SDK client con socket
@@ -0,0 +1,203 @@
1
+ # Piano: Advanced Notifier Recorder mixin e pipeline unificata
2
+
3
+ Piano per un’estensiva modifica al plugin Advanced Notifier: **sostituire** decoder, ffmpeg audio e recorder attuali con **una sola pipeline** che supporti clip on-demand, recording con retention (motion, detection, ecc.) e spostare tutta la logica eventi/recording in un nuovo mixin **Advanced Notifier Recorder**.
4
+
5
+ ---
6
+
7
+ ## 1. Situazione attuale (da sostituire)
8
+
9
+ ### 1.1 Componenti separati
10
+
11
+ | Componente | File | Ruolo | Input | Output |
12
+ |------------|------|--------|-------|--------|
13
+ | **Decoder** | `cameraMixin.ts` | Loop frame per motion/detection | `getVideoStream(decoderStream)` (solo video, no audio) | JPEG in `lastFrame` + `storeDecoderFrame()` |
14
+ | **Audio** | `audioAnalyzerUtils.ts` | Analisi volume/classificazione | RTSP → ffmpeg `-vn -dn -sn` → PCM 16kHz mono | Eventi `audio` → `processAudioDetection` → `addMotionEvent` |
15
+ | **Recording** | `videoRecorderUtils.ts` | Clip su trigger (recording rules) | RTSP → ffmpeg `-c:v copy|libx264` (no audio in pratica) | `.mp4` in `recordedEvents/` |
16
+
17
+ Problemi:
18
+ - **Tre consumi separati** dello stream (decoder, audio ffmpeg, recording ffmpeg) = più connessioni RTSP e più carico.
19
+ - **Nessuna pipeline unica** video+audio: decoder senza audio, recorder senza audio reale, audio da secondo ffmpeg.
20
+ - **Eventi e recording** sono in `cameraMixin` + `main.ts`; nessun modulo dedicato “recorder/events”.
21
+
22
+ ### 1.2 Dove vivono eventi e clip oggi
23
+
24
+ - **Scrittura eventi:** `main.ts` → `storeEventImage()`, `addMotionEvent()` → `enqueueDbWrite` → `writeEventsAndMotionBatch()` in `db.ts` (path `storagePath/{deviceId}/events/dbs/{YYYYMMDD}.json`).
25
+ - **Trigger recording:** `cameraMixin.processAccumulatedDetections()` → `startRecording({ triggerTime, rules, candidates })`; prolungamento da `ensureRecordingMotionCheckInterval()`.
26
+ - **Clip:** `cameraMixin.getVideoClipsInternal()` legge da `recordedEventsPath` e da rule-generated path; `VideoRtspFfmpegRecorder` scrive in `recordedEvents/`.
27
+
28
+ ---
29
+
30
+ ## 2. Obiettivi
31
+
32
+ 1. **Una sola pipeline** per dispositivo camera:
33
+ - Un input (stream video, con o senza audio).
34
+ - Da lì: **frame per analysis** (motion/detection), **audio** (se presente), **segmenti di recording** (buffer/scratch + clip finali).
35
+ 2. **Clip on-demand:** generazione clip a partire da un intervallo temporale (es. “ultimi 30 s”, o “dalle 12:00:00 per 60 s”) usando la pipeline, senza avviare un secondo ffmpeg ad hoc.
36
+ 3. **Recording con retention rules:** continuare a supportare “record on motion / on detection” con regole configurabili; retention (es. “tieni 7 giorni”, “solo eventi con persona”) gestita nel nuovo mixin.
37
+ 4. **Nuovo mixin “Advanced Notifier Recorder”:** contiene tutta la logica eventi + recording + clip; il camera mixin resta “analysis + notifiche”, il plugin orchestra e espone API.
38
+
39
+ ---
40
+
41
+ ## 3. Architettura target
42
+
43
+ ### 3.1 Pipeline unica (per camera)
44
+
45
+ ```
46
+ ┌─────────────────────────────────────────────────────────┐
47
+ │ UNIFIED RECORDER PIPELINE │
48
+ Stream (RTSP/ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │
49
+ getVideoStream) │ │ Ingest │───▶│ Circular │───▶│ Outputs │ │
50
+ ─────────────────▶│ │ (demux + │ │ Buffer │ │ - Analysis │ │
51
+ │ │ optional │ │ (e.g. 60s) │ │ frames │ │
52
+ │ │ audio) │ │ │ │ - Audio │ │
53
+ │ └─────────────┘ └──────┬───────┘ │ chunks │ │
54
+ │ │ │ │ - Clips │ │
55
+ │ │ │ │ (on-demand │ │
56
+ │ │ │ │ or rule) │ │
57
+ │ │ ▼ └─────────────┘ │
58
+ │ │ ┌──────────────┐ │
59
+ │ └───────────│ Retention │ │
60
+ │ │ & clip │ │
61
+ │ │ writer │ │
62
+ └─────────────────────┴──────────────┴─────────────────────┘
63
+ ```
64
+
65
+ - **Ingest:** un processo ffmpeg (o un solo consumer Scrypted) che legge **un** stream (video + audio se disponibile): demux, decode video (e opzionale audio), scrive in un **buffer circolare** (segmenti in memoria o su disco, es. anelli da 60 s).
66
+ - **Outputs dalla pipeline:**
67
+ - **Analysis:** copia frame (o callback) verso il decoder esistente / motion / detection (così il camera mixin continua a ricevere frame senza aprire un secondo stream).
68
+ - **Audio:** stessi chunk PCM usati per analisi (soglie, YAMNET) e, se serve, per mux nei clip.
69
+ - **Clips:**
70
+ - **On-demand:** da buffer circolare + eventuale “tail live” → un segmento [start, end] → file .mp4 (o altro) generato dalla pipeline (es. segmenti già in formato adatto, o un secondo pass ffmpeg breve).
71
+ - **Retention rules:** quando una regola dice “record”, la pipeline scrive da buffer + live in un file in `recordedEvents/` (o path configurato), con possibile post-processing (thumbnail, metadati).
72
+
73
+ ### 3.2 Nuovo mixin: Advanced Notifier Recorder
74
+
75
+ - **Nome proposto:** `AdvancedNotifierRecorderMixin` (file es. `src/recorderMixin.ts`).
76
+ - **Interfacce Scrypted da considerare:** `EventRecorder`, `VideoClips` (se già usate), più l’eventuale nuova interfaccia per “clip on-demand” (es. `getClipForTimeRange(deviceId, startTime, endTime)`).
77
+ - **Responsabilità:**
78
+ - **Eventi:** ricevere “eventi” e “motion” dal camera mixin (o dalla pipeline) e scriverli nel DB (delega a `main.ts` per `enqueueDbWrite` o incorpora la logica se si sposta anche la queue nel recorder).
79
+ - **Recording:** gestione regole di recording (motion, detection, retention); avvio/arresto segmenti di recording tramite la **pipeline unica** (non più `VideoRtspFfmpegRecorder` separato).
80
+ - **Clip on-demand:** esporre API per “genera clip per [deviceId, start, end]” usando il buffer + writer della pipeline.
81
+ - **Retention:** pulizia clip/segmenti secondo retention rules (giorni, tipo evento, spazio disco); possibile integrazione con “rimuovi clip più vecchi di X” già presente in `main.ts`.
82
+
83
+ ### 3.3 Ruolo del camera mixin dopo il refactor
84
+
85
+ - **Conservare:** regole detection, notifiche, occupancy, timelapse, UI settings (decoder type, stream destination, ecc.).
86
+ - **Cambiare:**
87
+ - **Decoder:** non più un loop che chiama `getVideoStream()` da solo; invece **riceve frame dalla pipeline** del recorder (o legge da un’API del recorder “get next frame for analysis”). In questo modo c’è un solo consumer dello stream.
88
+ - **Audio:** non più `AudioRtspFfmpegStream` nel camera mixin; il recorder espone chunk audio (o callback) e il camera mixin continua a chiamare `processAudioDetection` con quei chunk.
89
+ - **Recording:** nessuna chiamata a `startRecording` / `VideoRtspFfmpegRecorder` dal camera mixin; il camera mixin segnala al recorder “c’è un evento/motion che matcha una recording rule” e il **recorder** avvia/ prolunga il segmento tramite la pipeline.
90
+ - **Eventi:** il camera mixin può continuare a chiamare `plugin.storeEventImage()` e `plugin.addMotionEvent()`; l’implementazione di queste può essere spostata nel recorder mixin (e il plugin le delega al recorder), così tutta la “scrittura eventi” è in un posto.
91
+
92
+ ### 3.4 Plugin (main.ts)
93
+
94
+ - **Composizione:** oltre a `AdvancedNotifierCameraMixin` e `AdvancedNotifierNotifierMixin`, introdurre `AdvancedNotifierRecorderMixin`.
95
+ - Opzione A: il **recorder è un mixin sulla stessa camera** (stesso device, tre mixin: notifier, camera, recorder). La pipeline unica è di proprietà del recorder; camera e notifier la “usano” tramite il recorder.
96
+ - Opzione B: il recorder è un **device separato** “Recorder” per camera (uno-a-uno). La pipeline vive nel device Recorder; la camera mixin comunica con esso via plugin.
97
+ - **Percorsi e storage:** `getRecordedEventPath`, `getEventPaths`, `storeEventImage`, `addMotionEvent` possono restare in `main.ts` come facade che delega al recorder mixin (per device camera), così l’API pubblica del plugin non cambia.
98
+ - **DB queue:** `dbWriteQueue` / `enqueueDbWrite` / `runDbWriteProcess` possono restare in `main.ts` o essere spostati nel recorder; il recorder in ogni caso deve poter scrivere eventi/motion nel DB.
99
+
100
+ ---
101
+
102
+ ## 4. Pipeline unica: dettaglio tecnico
103
+
104
+ ### 4.1 Scelta implementativa
105
+
106
+ - **Opzione 1 – FFmpeg unico (demux + buffer + tee):** un processo ffmpeg che:
107
+ - Legge RTSP (o riceve stream da Scrypted).
108
+ - Demux video + audio.
109
+ - Scrive in un **segment file** circolare (es. `segment_%03d.m4s` o simile) o in un **named pipe / shared memory** letto da Node.
110
+ - Opzionale: `tee` per inviare copia a un secondo output (es. analisi).
111
+ - Pro: un solo processo, meno connessioni. Contro: complessità buffer/segmenti e sincronizzazione con “clip da intervallo”.
112
+ - **Opzione 2 – Consumer Scrypted + buffer in Node:** un solo `getVideoStream()` (con audio se il backend lo supporta); in Node un consumer che:
113
+ - Legge frame (e eventuale audio) e li mette in un **buffer circolare** (es. anello di segmenti in memoria o file).
114
+ - Espone “slice del buffer” per clip on-demand e per “scrivi da start a end” per recording.
115
+ - Pro: massimo controllo in JS. Contro: possibile overhead e complessità (codec, mux) se i frame arrivano già codificati.
116
+ - **Opzione 3 – Ibrido:** ffmpeg per ingest e buffer su disco (segmenti brevi, es. 5–10 s); un servizio in Node che tiene un indice (startTime → file) e per clip on-demand concatena/rimux con ffmpeg. Recording “su regola” = copia di segmenti già scritti + append live fino a fine evento.
117
+
118
+ Raccomandazione: partire da **Opzione 3** per avere un buffer su disco ben definito e clip on-demand affidabili; unificare comunque in **un solo ffmpeg di ingest** (video+audio) che produce segmenti, e un “RecorderPipeline” in Node che gestisce indice, retention e generazione clip.
119
+
120
+ ### 4.2 Buffer circolare / segmenti
121
+
122
+ - **Formato:** segmenti brevi (es. 5–15 s) in formato adatto al concatenamento (es. fMP4 o segmenti MPEG-TS).
123
+ - **Indice:** struttura (in memoria o file) che mappa `[startTime, endTime]` → lista file segmenti.
124
+ - **Retention:** job periodico che rimuove segmenti oltre la retention (o oltre lo spazio massimo); i clip “recorded” (salvati in `recordedEvents/`) sono copie permanenti fino a quando non scatta la loro retention.
125
+
126
+ ### 4.3 Clip on-demand
127
+
128
+ - **Input:** `deviceId`, `startTime`, `endTime` (timestamp Unix o ms).
129
+ - **Logica:** dalla pipeline (indice segmenti) individuare i segmenti che coprono [startTime, endTime]; concatenare (concat demuxer ffmpeg o copy) e scrivere un file .mp4; opzionale: estrarre thumbnail a metà clip.
130
+ - **Output:** path del file clip (e thumbnail) da esporre via API (es. `VideoClips.getVideoClip` o nuova `getClipForTimeRange`).
131
+
132
+ ### 4.4 Recording con retention rules
133
+
134
+ - **Regole:** come oggi (motion, classi detection, ecc.) ma interpretate dal **recorder mixin**.
135
+ - **Trigger:** il camera mixin (o la pipeline) segnala “motion on” / “detection X”; il recorder confronta con le regole e decide “start recording” / “prolong”.
136
+ - **Scrittura:** invece di avviare un `VideoRtspFfmpegRecorder` separato, il recorder dice alla pipeline “da adesso scrivi in un file in `recordedEvents/` fino a fine evento (o max duration)”. La pipeline può:
137
+ - copiare dal buffer (segmenti già scritti) per la parte “pre-trigger” (es. 30 s prima),
138
+ - poi appendere live fino a “motion off” + post-buffer.
139
+ - **Retention:** regole tipo “conserva 7 giorni”, “solo eventi con persona”; il recorder applica la pulizia sui file in `recordedEvents/` (e eventualmente sui segmenti del buffer).
140
+
141
+ ---
142
+
143
+ ## 5. Piano di implementazione (fasi)
144
+
145
+ ### Fase 1 – Fondamenta recorder e spostamento eventi
146
+
147
+ 1. **Creare `recorderMixin.ts`** (Advanced Notifier Recorder mixin).
148
+ - Interfacce: almeno ciò che serve per “eventi” e “clip” (EventRecorder / VideoClips se già usate).
149
+ - Implementare **delega** di `storeEventImage` e `addMotionEvent`: il plugin, quando è un device camera con recorder mixin, inoltra al recorder; il recorder chiama la stessa logica di scrittura DB (o sposta `enqueueDbWrite` nel recorder).
150
+ 2. **Registrare il mixin in `main.ts`:** per le camera, creare anche il recorder mixin (stesso device o device figlio); mantenere l’API `storeEventImage` / `addMotionEvent` sul plugin che delega al recorder.
151
+ 3. **Test:** verificare che eventi e motion continuino a essere scritti e letti come oggi (Events App, Data Fetcher).
152
+
153
+ ### Fase 2 – Pipeline unica (ingest + buffer)
154
+
155
+ 1. **Modulo “RecorderPipeline”** (es. `src/recorderPipeline.ts` o sotto `src/recorder/`):
156
+ - Ingest: un processo ffmpeg che legge **un** stream (RTSP o URL da `getVideoStream` se possibile) con **video + audio**, output in segmenti (fMP4 o TS).
157
+ - Parametri: cameraId, stream URL, path directory segmenti, lunghezza segmento, lunghezza buffer (es. 60 s = 12 segmenti da 5 s).
158
+ - Scrittura segmenti e indice (startTime/endTime per segmento).
159
+ 2. **Integrazione nel recorder mixin:** all’avvio della camera (o on-demand quando serve recording/clip), avviare la pipeline per quella camera; fermarla quando la camera viene rilasciata.
160
+ 3. **Sostituire l’audio analyzer:** invece di avviare `AudioRtspFfmpegStream`, il recorder legge l’audio dalla pipeline (dai segmenti o da un output ffmpeg dedicato “solo audio” tee). Fornire i chunk al camera mixin per `processAudioDetection` (stessa API).
161
+ 4. **Sostituire il decoder:** il decoder non chiama più `getVideoStream()` direttamente; la pipeline espone “frame per analysis” (es. estrazione frame dai segmenti con ffmpeg, o tee video verso un output che il camera mixin consuma). Il camera mixin continua a fare motion/detection sui frame così forniti.
162
+
163
+ ### Fase 3 – Recording e clip dalla pipeline
164
+
165
+ 1. **Recording:** rimuovere `VideoRtspFfmpegRecorder` e `startRecording` dal camera mixin. Nel recorder:
166
+ - Alla notifica “start recording” (da camera mixin o da regole interne), chiedere alla pipeline di “salvare da buffer[start] a live fino a stop”.
167
+ - Implementare “prolong on motion” leggendo lo stato motion dalla pipeline/camera mixin.
168
+ 2. **Clip on-demand:** implementare `getClipForTimeRange(deviceId, startTime, endTime)` (o equivalente) usando l’indice segmenti; concatenare e scrivere .mp4; restituire path o URL.
169
+ 3. **Retention:** job nel recorder che applica retention rules su `recordedEvents/` e sui segmenti del buffer; integrare con la logica di rimozione clip già presente in `main.ts` (es. spostarla nel recorder).
170
+
171
+ ### Fase 4 – Pulizia e opzionali
172
+
173
+ 1. **Rimuovere** da `cameraMixin.ts`: `startRecording`, `stopRecording`, `ensureRecordingMotionCheckInterval`, uso di `VideoRtspFfmpegRecorder`, `AudioRtspFfmpegStream` (sostituito dalla pipeline), e il loop decoder “standalone” (sostituito da frame dalla pipeline).
174
+ 2. **Deprecare** (o rimuovere) `videoRecorderUtils.ts` e `audioAnalyzerUtils.ts` nella forma attuale; eventualmente tenere helper riutilizzabili (es. estrazione thumbnail) dentro il modulo pipeline/recorder.
175
+ 3. **Documentazione:** aggiornare README e doc per “single pipeline”, “recorder mixin”, “retention rules”.
176
+ 4. **Settings:** spostare le impostazioni “recording rules”, “retention”, “buffer length”, “decoder source” (pipeline vs legacy, se si mantiene fallback) nel recorder mixin o in una sezione “Recording” condivisa.
177
+
178
+ ---
179
+
180
+ ## 6. Riepilogo file toccati / nuovi
181
+
182
+ | Azione | File |
183
+ |--------|------|
184
+ | **Nuovo** | `src/recorderMixin.ts` – Advanced Notifier Recorder mixin (eventi, recording, clip, retention). |
185
+ | **Nuovo** | `src/recorderPipeline.ts` (o `src/recorder/`) – Ingest ffmpeg, buffer segmenti, indice, export clip. |
186
+ | **Modifica** | `src/main.ts` – Registrazione recorder mixin, delega `storeEventImage`/`addMotionEvent` al recorder, eventuale spostamento DB queue. |
187
+ | **Modifica** | `src/cameraMixin.ts` – Rimuovere decoder standalone, audio analyzer, startRecording/VideoRtspFfmpegRecorder; ricevere frame e audio dalla pipeline/recorder; mantenere detection, notifiche, regole. |
188
+ | **Modifica** | `src/db.ts` – Solo se la scrittura eventi viene spostata nel recorder (stesso schema, diverso chiamante). |
189
+ | **Deprecare/rimuovere** | `src/videoRecorderUtils.ts` – Sostituito dalla pipeline. |
190
+ | **Deprecare/rimuovere** | `src/audioAnalyzerUtils.ts` – Sostituito da audio dalla pipeline. |
191
+
192
+ ---
193
+
194
+ ## 7. Rischi e mitigazioni
195
+
196
+ - **Compatibilità:** mantenere l’API pubblica del plugin (EventRecorder, VideoClips, getVideoClips, getRecordedEventPath, storeEventImage, addMotionEvent) così che camstack e altri client non cambino.
197
+ - **Performance:** un solo ffmpeg per camera può essere un single point of failure; prevedere restart automatico e backoff come in `VideoRtspFfmpegRecorder`/`AudioRtspFfmpegStream`.
198
+ - **Disco:** il buffer circolare su disco consuma spazio; configurare lunghezza massima e retention chiara.
199
+ - **Migrazione:** per rollout graduale, si può mantenere un “legacy mode” (decoder + audio ffmpeg + VideoRtspFfmpegRecorder) disattivabile da setting “Use unified recorder pipeline”, e abilitare la nuova pipeline solo quando il setting è on.
200
+
201
+ ---
202
+
203
+ Questo piano può essere usato come base per issue, task e PR incrementali (una fase per volta).
@@ -0,0 +1 @@
1
+ .react-resizable{position:relative}.react-resizable-handle{box-sizing:border-box;background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+");background-position:100% 100%;background-repeat:no-repeat;background-origin:content-box;width:20px;height:20px;padding:0 3px 3px 0;position:absolute}.react-resizable-handle-sw{cursor:sw-resize;bottom:0;left:0;transform:rotate(90deg)}.react-resizable-handle-se{cursor:se-resize;bottom:0;right:0}.react-resizable-handle-nw{cursor:nw-resize;top:0;left:0;transform:rotate(180deg)}.react-resizable-handle-ne{cursor:ne-resize;top:0;right:0;transform:rotate(270deg)}.react-resizable-handle-w,.react-resizable-handle-e{cursor:ew-resize;margin-top:-10px;top:50%}.react-resizable-handle-w{left:0;transform:rotate(135deg)}.react-resizable-handle-e{right:0;transform:rotate(315deg)}.react-resizable-handle-n,.react-resizable-handle-s{cursor:ns-resize;margin-left:-10px;left:50%}.react-resizable-handle-n{top:0;transform:rotate(225deg)}.react-resizable-handle-s{bottom:0;transform:rotate(45deg)}
@@ -0,0 +1 @@
1
+ .react-grid-layout{transition:height .2s;position:relative}.react-grid-item{transition:left .2s,top .2s,width .2s,height .2s}.react-grid-item img{pointer-events:none;-webkit-user-select:none;user-select:none}.react-grid-item.cssTransforms{transition-property:transform,width,height}.react-grid-item.resizing{z-index:1;will-change:width, height;transition:none}.react-grid-item.react-draggable-dragging{z-index:3;will-change:transform;transition:none}.react-grid-item.dropping{visibility:hidden}.react-grid-item.react-grid-placeholder{opacity:.2;z-index:2;-webkit-user-select:none;user-select:none;background:red;transition-duration:.1s}.react-grid-item.react-grid-placeholder.placeholder-resizing{transition:none}.react-grid-item>.react-resizable-handle{opacity:0;width:20px;height:20px;position:absolute}.react-grid-item:hover>.react-resizable-handle{opacity:1}.react-grid-item>.react-resizable-handle:after{content:"";border-bottom:2px solid #0006;border-right:2px solid #0006;width:5px;height:5px;position:absolute;bottom:3px;right:3px}.react-resizable-hide>.react-resizable-handle{display:none}.react-grid-item>.react-resizable-handle.react-resizable-handle-sw{cursor:sw-resize;bottom:0;left:0;transform:rotate(90deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-se{cursor:se-resize;bottom:0;right:0}.react-grid-item>.react-resizable-handle.react-resizable-handle-nw{cursor:nw-resize;top:0;left:0;transform:rotate(180deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-ne{cursor:ne-resize;top:0;right:0;transform:rotate(270deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-w,.react-grid-item>.react-resizable-handle.react-resizable-handle-e{cursor:ew-resize;margin-top:-10px;top:50%}.react-grid-item>.react-resizable-handle.react-resizable-handle-w{left:0;transform:rotate(135deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-e{right:0;transform:rotate(315deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-n,.react-grid-item>.react-resizable-handle.react-resizable-handle-s{cursor:ns-resize;margin-left:-10px;left:50%}.react-grid-item>.react-resizable-handle.react-resizable-handle-n{top:0;transform:rotate(225deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-s{bottom:0;transform:rotate(45deg)}