Skip to main content

dahua_camera_server/
dto.rs

1//! Wire types for the JSON API.
2//!
3//! Kept separate from the domain deliberately. The API is a published contract
4//! with the diagnostics UI and the Tauri shell; letting it be whatever the
5//! domain structs happen to serialise to means an internal refactor silently
6//! breaks a client.
7
8use dahua_camera_rtsp::Camera;
9use dahua_camera_core::CameraStatus;
10use serde::Serialize;
11use std::sync::atomic::Ordering;
12
13/// One camera, as `GET /api/cameras` reports it.
14#[derive(Debug, Clone, Serialize)]
15pub struct CameraView {
16    /// Stable identifier.
17    pub id: String,
18    /// Display name, falling back to the id.
19    pub label: String,
20    /// Current lifecycle state.
21    pub status: CameraStatus,
22    /// Attached consumers.
23    pub viewers: usize,
24    /// Samples received since startup.
25    pub frames: u64,
26    /// Random-access points received since startup.
27    pub idr_frames: u64,
28    /// Encoded bytes received since startup.
29    pub bytes: u64,
30    /// Session restarts since startup.
31    pub reconnects: u64,
32    /// Samples dropped by lagging subscribers.
33    pub lagged_drops: u64,
34    /// Frames pushed through the decode lane.
35    pub decoded_frames: u64,
36    /// Coded width, once known.
37    pub width: Option<u16>,
38    /// Coded height, once known.
39    pub height: Option<u16>,
40}
41
42impl CameraView {
43    /// Snapshot a camera's current state.
44    ///
45    /// Counters are read independently and may be very slightly inconsistent
46    /// with each other; they are for humans watching a dashboard, not for
47    /// anything that needs a coherent instant.
48    pub fn of(camera: &Camera) -> Self {
49        let params = camera.params();
50        let stats = camera.stats();
51        Self {
52            id: camera.id().to_owned(),
53            label: camera.config().display_name().to_owned(),
54            status: camera.status(),
55            viewers: camera.viewer_count(),
56            frames: stats.frames.load(Ordering::Relaxed),
57            idr_frames: stats.idr_frames.load(Ordering::Relaxed),
58            bytes: stats.bytes.load(Ordering::Relaxed),
59            reconnects: stats.reconnects.load(Ordering::Relaxed),
60            lagged_drops: stats.lagged_drops.load(Ordering::Relaxed),
61            decoded_frames: stats.decoded_frames.load(Ordering::Relaxed),
62            width: params.as_ref().map(|p| p.codec.width),
63            height: params.as_ref().map(|p| p.codec.height),
64        }
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn status_serialises_as_a_tagged_object() {
74        let json = serde_json::to_value(CameraStatus::Streaming { width: 704, height: 576 })
75            .expect("serialises");
76        assert_eq!(json["state"], "streaming");
77        assert_eq!(json["width"], 704);
78    }
79
80    #[test]
81    fn idle_status_carries_no_extra_fields() {
82        let json = serde_json::to_value(CameraStatus::Idle).expect("serialises");
83        assert_eq!(json["state"], "idle");
84        assert_eq!(json.as_object().expect("object").len(), 1);
85    }
86
87    #[test]
88    fn reconnecting_status_reports_the_delay_and_reason() {
89        let json = serde_json::to_value(CameraStatus::Reconnecting {
90            after_secs: 4,
91            reason: "stream ended".into(),
92        })
93        .expect("serialises");
94        assert_eq!(json["state"], "reconnecting");
95        assert_eq!(json["after_secs"], 4);
96        assert_eq!(json["reason"], "stream ended");
97    }
98}