Skip to main content

heldar_kernel/models/
camera.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use sqlx::types::Json;
5use sqlx::FromRow;
6
7use crate::camera_url;
8
9/// Camera row as stored. `password` is never serialized to clients; use [`CameraView`] for output.
10#[derive(Debug, Clone, FromRow)]
11pub struct Camera {
12    pub id: String,
13    pub site_id: Option<String>,
14    pub name: String,
15    pub vendor: String,
16    pub model: Option<String>,
17    pub address: Option<String>,
18    pub rtsp_port: i64,
19    pub username: Option<String>,
20    pub password: Option<String>,
21    pub main_stream_url: Option<String>,
22    pub sub_stream_url: Option<String>,
23    pub record_stream: String,
24    pub codec: Option<String>,
25    pub resolution_main: Option<String>,
26    pub resolution_sub: Option<String>,
27    pub fps_main: Option<i64>,
28    pub fps_sub: Option<i64>,
29    pub capabilities: Json<Value>,
30    pub record_enabled: bool,
31    pub segment_seconds: i64,
32    pub retention_hours: i64,
33    /// Per-camera storage quota in bytes; NULL means no per-camera cap.
34    pub storage_quota_bytes: Option<i64>,
35    /// Record the camera's audio stream (pass-through) instead of dropping it.
36    pub record_audio: bool,
37    /// When the recorder runs: `continuous` | `scheduled` | `event` | `scheduled_event`.
38    pub record_mode: String,
39    /// Event recording: footage desired BEFORE a trigger (best-effort, see recorder service).
40    pub pre_roll_seconds: i64,
41    /// Event recording: how long the recorder keeps writing after a trigger (the trigger window).
42    pub post_roll_seconds: i64,
43    /// Run a SECOND ffmpeg pipeline writing identical segments to HELDAR_MIRROR_RECORDINGS_DIR
44    /// (redundant DVR copy). No-op unless the mirror dir is configured.
45    pub mirror_enabled: bool,
46    /// Let the ANR loop re-fetch missed footage from the camera's onboard storage to fill gaps.
47    pub anr_enabled: bool,
48    /// Optional replay URL template for ANR ({start}/{end} placeholders); NULL = default Hikvision
49    /// RTSP playback built from address+credentials.
50    pub anr_replay_url_template: Option<String>,
51    pub enabled: bool,
52    /// AI decode priority (higher = more important). The frame sampler favors high-priority cameras
53    /// under fps-budget pressure and sheds low-priority ones first.
54    pub priority: i64,
55    pub created_at: DateTime<Utc>,
56    pub updated_at: DateTime<Utc>,
57}
58
59impl Camera {
60    /// Whether the recorder should be running a process for this camera.
61    pub fn should_record(&self) -> bool {
62        self.enabled && self.record_enabled
63    }
64}
65
66/// Client-facing camera representation: credentials stripped, stream URLs masked.
67#[derive(Debug, Clone, Serialize)]
68pub struct CameraView {
69    pub id: String,
70    pub site_id: Option<String>,
71    pub name: String,
72    pub vendor: String,
73    pub model: Option<String>,
74    pub address: Option<String>,
75    pub rtsp_port: i64,
76    pub username: Option<String>,
77    pub has_password: bool,
78    pub record_stream: String,
79    /// Effective RTSP URL for the recorded stream, with credentials masked.
80    pub record_url_masked: Option<String>,
81    pub codec: Option<String>,
82    pub resolution_main: Option<String>,
83    pub resolution_sub: Option<String>,
84    pub fps_main: Option<i64>,
85    pub fps_sub: Option<i64>,
86    pub capabilities: Value,
87    pub record_enabled: bool,
88    pub segment_seconds: i64,
89    pub retention_hours: i64,
90    pub storage_quota_bytes: Option<i64>,
91    pub record_audio: bool,
92    pub record_mode: String,
93    pub pre_roll_seconds: i64,
94    pub post_roll_seconds: i64,
95    pub mirror_enabled: bool,
96    pub anr_enabled: bool,
97    pub anr_replay_url_template: Option<String>,
98    pub enabled: bool,
99    pub priority: i64,
100    pub created_at: DateTime<Utc>,
101    pub updated_at: DateTime<Utc>,
102}
103
104impl From<Camera> for CameraView {
105    fn from(c: Camera) -> Self {
106        let record_url_masked = camera_url::record_url(&c).map(|u| camera_url::mask_url(&u));
107        CameraView {
108            id: c.id,
109            site_id: c.site_id,
110            name: c.name,
111            vendor: c.vendor,
112            model: c.model,
113            address: c.address,
114            rtsp_port: c.rtsp_port,
115            username: c.username,
116            has_password: c
117                .password
118                .as_deref()
119                .map(|p| !p.is_empty())
120                .unwrap_or(false),
121            record_stream: c.record_stream,
122            record_url_masked,
123            codec: c.codec,
124            resolution_main: c.resolution_main,
125            resolution_sub: c.resolution_sub,
126            fps_main: c.fps_main,
127            fps_sub: c.fps_sub,
128            capabilities: c.capabilities.0,
129            record_enabled: c.record_enabled,
130            segment_seconds: c.segment_seconds,
131            retention_hours: c.retention_hours,
132            storage_quota_bytes: c.storage_quota_bytes,
133            record_audio: c.record_audio,
134            record_mode: c.record_mode,
135            pre_roll_seconds: c.pre_roll_seconds,
136            post_roll_seconds: c.post_roll_seconds,
137            mirror_enabled: c.mirror_enabled,
138            anr_enabled: c.anr_enabled,
139            anr_replay_url_template: c.anr_replay_url_template,
140            enabled: c.enabled,
141            priority: c.priority,
142            created_at: c.created_at,
143            updated_at: c.updated_at,
144        }
145    }
146}
147
148/// Payload to create a camera. `id` may be omitted (slug auto-derived from name).
149#[derive(Debug, Deserialize)]
150pub struct CameraCreate {
151    pub id: Option<String>,
152    pub name: String,
153    pub site_id: Option<String>,
154    #[serde(default = "default_vendor")]
155    pub vendor: String,
156    pub model: Option<String>,
157    pub address: Option<String>,
158    pub rtsp_port: Option<i64>,
159    pub username: Option<String>,
160    pub password: Option<String>,
161    pub main_stream_url: Option<String>,
162    pub sub_stream_url: Option<String>,
163    pub record_stream: Option<String>,
164    pub capabilities: Option<Value>,
165    pub record_enabled: Option<bool>,
166    pub segment_seconds: Option<i64>,
167    pub retention_hours: Option<i64>,
168    pub storage_quota_bytes: Option<i64>,
169    pub record_audio: Option<bool>,
170    pub record_mode: Option<String>,
171    pub pre_roll_seconds: Option<i64>,
172    pub post_roll_seconds: Option<i64>,
173    pub mirror_enabled: Option<bool>,
174    pub anr_enabled: Option<bool>,
175    pub anr_replay_url_template: Option<String>,
176    pub enabled: Option<bool>,
177}
178
179fn default_vendor() -> String {
180    "generic".to_string()
181}
182
183/// Partial update; only present fields are changed.
184#[derive(Debug, Deserialize, Default)]
185pub struct CameraUpdate {
186    pub name: Option<String>,
187    pub site_id: Option<String>,
188    pub vendor: Option<String>,
189    pub model: Option<String>,
190    pub address: Option<String>,
191    pub rtsp_port: Option<i64>,
192    pub username: Option<String>,
193    pub password: Option<String>,
194    pub main_stream_url: Option<String>,
195    pub sub_stream_url: Option<String>,
196    pub record_stream: Option<String>,
197    pub capabilities: Option<Value>,
198    pub record_enabled: Option<bool>,
199    pub segment_seconds: Option<i64>,
200    pub retention_hours: Option<i64>,
201    pub storage_quota_bytes: Option<i64>,
202    pub record_audio: Option<bool>,
203    pub record_mode: Option<String>,
204    pub pre_roll_seconds: Option<i64>,
205    pub post_roll_seconds: Option<i64>,
206    pub mirror_enabled: Option<bool>,
207    pub anr_enabled: Option<bool>,
208    pub anr_replay_url_template: Option<String>,
209    pub enabled: Option<bool>,
210    pub priority: Option<i64>,
211}
212
213#[derive(Debug, Clone, Serialize, FromRow)]
214pub struct Segment {
215    pub id: String,
216    pub camera_id: String,
217    pub path: String,
218    pub start_time: DateTime<Utc>,
219    pub end_time: DateTime<Utc>,
220    pub duration_s: f64,
221    pub codec: Option<String>,
222    pub width: Option<i64>,
223    pub height: Option<i64>,
224    pub size_bytes: i64,
225    pub container: String,
226    /// Transient read-lock held by clip/snapshot export; cleared at startup. Not durable.
227    pub locked: bool,
228    /// Durable evidence hold: when true the segment is never pruned by retention. Set via the
229    /// incident API; survives restarts (unlike `locked`).
230    pub evidence_locked: bool,
231    pub incident_id: Option<String>,
232    pub created_at: DateTime<Utc>,
233}
234
235/// A recording gap detected by the indexer (a hole > 3s between consecutive segments). The ANR loop
236/// (services/anr.rs) tries to re-fill pending gaps from the camera's onboard storage. `fill_state` is
237/// `pending` | `filled` | `failed`.
238#[derive(Debug, Clone, Serialize, FromRow)]
239pub struct RecordingGap {
240    pub id: String,
241    pub camera_id: String,
242    pub gap_start: DateTime<Utc>,
243    pub gap_end: DateTime<Utc>,
244    pub gap_seconds: i64,
245    pub fill_state: String,
246    pub fill_attempts: i64,
247    pub last_attempt_at: Option<DateTime<Utc>>,
248    pub filled_at: Option<DateTime<Utc>>,
249    pub created_at: DateTime<Utc>,
250}
251
252#[derive(Debug, Clone, Serialize, FromRow)]
253pub struct CameraStatus {
254    pub camera_id: String,
255    pub state: String,
256    pub last_segment_at: Option<DateTime<Utc>>,
257    pub last_started_at: Option<DateTime<Utc>>,
258    pub reconnect_count: i64,
259    pub segments_written: i64,
260    pub fps_observed: Option<f64>,
261    pub bitrate_kbps: Option<f64>,
262    pub last_error: Option<String>,
263    pub recorder_pid: Option<i64>,
264    pub updated_at: DateTime<Utc>,
265}