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    pub created_at: DateTime<Utc>,
53    pub updated_at: DateTime<Utc>,
54}
55
56impl Camera {
57    /// Whether the recorder should be running a process for this camera.
58    pub fn should_record(&self) -> bool {
59        self.enabled && self.record_enabled
60    }
61}
62
63/// Client-facing camera representation: credentials stripped, stream URLs masked.
64#[derive(Debug, Clone, Serialize)]
65pub struct CameraView {
66    pub id: String,
67    pub site_id: Option<String>,
68    pub name: String,
69    pub vendor: String,
70    pub model: Option<String>,
71    pub address: Option<String>,
72    pub rtsp_port: i64,
73    pub username: Option<String>,
74    pub has_password: bool,
75    pub record_stream: String,
76    /// Effective RTSP URL for the recorded stream, with credentials masked.
77    pub record_url_masked: Option<String>,
78    pub codec: Option<String>,
79    pub resolution_main: Option<String>,
80    pub resolution_sub: Option<String>,
81    pub fps_main: Option<i64>,
82    pub fps_sub: Option<i64>,
83    pub capabilities: Value,
84    pub record_enabled: bool,
85    pub segment_seconds: i64,
86    pub retention_hours: i64,
87    pub storage_quota_bytes: Option<i64>,
88    pub record_audio: bool,
89    pub record_mode: String,
90    pub pre_roll_seconds: i64,
91    pub post_roll_seconds: i64,
92    pub mirror_enabled: bool,
93    pub anr_enabled: bool,
94    pub anr_replay_url_template: Option<String>,
95    pub enabled: bool,
96    pub created_at: DateTime<Utc>,
97    pub updated_at: DateTime<Utc>,
98}
99
100impl From<Camera> for CameraView {
101    fn from(c: Camera) -> Self {
102        let record_url_masked = camera_url::record_url(&c).map(|u| camera_url::mask_url(&u));
103        CameraView {
104            id: c.id,
105            site_id: c.site_id,
106            name: c.name,
107            vendor: c.vendor,
108            model: c.model,
109            address: c.address,
110            rtsp_port: c.rtsp_port,
111            username: c.username,
112            has_password: c
113                .password
114                .as_deref()
115                .map(|p| !p.is_empty())
116                .unwrap_or(false),
117            record_stream: c.record_stream,
118            record_url_masked,
119            codec: c.codec,
120            resolution_main: c.resolution_main,
121            resolution_sub: c.resolution_sub,
122            fps_main: c.fps_main,
123            fps_sub: c.fps_sub,
124            capabilities: c.capabilities.0,
125            record_enabled: c.record_enabled,
126            segment_seconds: c.segment_seconds,
127            retention_hours: c.retention_hours,
128            storage_quota_bytes: c.storage_quota_bytes,
129            record_audio: c.record_audio,
130            record_mode: c.record_mode,
131            pre_roll_seconds: c.pre_roll_seconds,
132            post_roll_seconds: c.post_roll_seconds,
133            mirror_enabled: c.mirror_enabled,
134            anr_enabled: c.anr_enabled,
135            anr_replay_url_template: c.anr_replay_url_template,
136            enabled: c.enabled,
137            created_at: c.created_at,
138            updated_at: c.updated_at,
139        }
140    }
141}
142
143/// Payload to create a camera. `id` may be omitted (slug auto-derived from name).
144#[derive(Debug, Deserialize)]
145pub struct CameraCreate {
146    pub id: Option<String>,
147    pub name: String,
148    pub site_id: Option<String>,
149    #[serde(default = "default_vendor")]
150    pub vendor: String,
151    pub model: Option<String>,
152    pub address: Option<String>,
153    pub rtsp_port: Option<i64>,
154    pub username: Option<String>,
155    pub password: Option<String>,
156    pub main_stream_url: Option<String>,
157    pub sub_stream_url: Option<String>,
158    pub record_stream: Option<String>,
159    pub capabilities: Option<Value>,
160    pub record_enabled: Option<bool>,
161    pub segment_seconds: Option<i64>,
162    pub retention_hours: Option<i64>,
163    pub storage_quota_bytes: Option<i64>,
164    pub record_audio: Option<bool>,
165    pub record_mode: Option<String>,
166    pub pre_roll_seconds: Option<i64>,
167    pub post_roll_seconds: Option<i64>,
168    pub mirror_enabled: Option<bool>,
169    pub anr_enabled: Option<bool>,
170    pub anr_replay_url_template: Option<String>,
171    pub enabled: Option<bool>,
172}
173
174fn default_vendor() -> String {
175    "generic".to_string()
176}
177
178/// Partial update; only present fields are changed.
179#[derive(Debug, Deserialize, Default)]
180pub struct CameraUpdate {
181    pub name: Option<String>,
182    pub site_id: Option<String>,
183    pub vendor: Option<String>,
184    pub model: Option<String>,
185    pub address: Option<String>,
186    pub rtsp_port: Option<i64>,
187    pub username: Option<String>,
188    pub password: Option<String>,
189    pub main_stream_url: Option<String>,
190    pub sub_stream_url: Option<String>,
191    pub record_stream: Option<String>,
192    pub capabilities: Option<Value>,
193    pub record_enabled: Option<bool>,
194    pub segment_seconds: Option<i64>,
195    pub retention_hours: Option<i64>,
196    pub storage_quota_bytes: Option<i64>,
197    pub record_audio: Option<bool>,
198    pub record_mode: Option<String>,
199    pub pre_roll_seconds: Option<i64>,
200    pub post_roll_seconds: Option<i64>,
201    pub mirror_enabled: Option<bool>,
202    pub anr_enabled: Option<bool>,
203    pub anr_replay_url_template: Option<String>,
204    pub enabled: Option<bool>,
205}
206
207#[derive(Debug, Clone, Serialize, FromRow)]
208pub struct Segment {
209    pub id: String,
210    pub camera_id: String,
211    pub path: String,
212    pub start_time: DateTime<Utc>,
213    pub end_time: DateTime<Utc>,
214    pub duration_s: f64,
215    pub codec: Option<String>,
216    pub width: Option<i64>,
217    pub height: Option<i64>,
218    pub size_bytes: i64,
219    pub container: String,
220    /// Transient read-lock held by clip/snapshot export; cleared at startup. Not durable.
221    pub locked: bool,
222    /// Durable evidence hold: when true the segment is never pruned by retention. Set via the
223    /// incident API; survives restarts (unlike `locked`).
224    pub evidence_locked: bool,
225    pub incident_id: Option<String>,
226    pub created_at: DateTime<Utc>,
227}
228
229/// A recording gap detected by the indexer (a hole > 3s between consecutive segments). The ANR loop
230/// (services/anr.rs) tries to re-fill pending gaps from the camera's onboard storage. `fill_state` is
231/// `pending` | `filled` | `failed`.
232#[derive(Debug, Clone, Serialize, FromRow)]
233pub struct RecordingGap {
234    pub id: String,
235    pub camera_id: String,
236    pub gap_start: DateTime<Utc>,
237    pub gap_end: DateTime<Utc>,
238    pub gap_seconds: i64,
239    pub fill_state: String,
240    pub fill_attempts: i64,
241    pub last_attempt_at: Option<DateTime<Utc>>,
242    pub filled_at: Option<DateTime<Utc>>,
243    pub created_at: DateTime<Utc>,
244}
245
246#[derive(Debug, Clone, Serialize, FromRow)]
247pub struct CameraStatus {
248    pub camera_id: String,
249    pub state: String,
250    pub last_segment_at: Option<DateTime<Utc>>,
251    pub last_started_at: Option<DateTime<Utc>>,
252    pub reconnect_count: i64,
253    pub segments_written: i64,
254    pub fps_observed: Option<f64>,
255    pub bitrate_kbps: Option<f64>,
256    pub last_error: Option<String>,
257    pub recorder_pid: Option<i64>,
258    pub updated_at: DateTime<Utc>,
259}