Skip to main content

dahua_camera_rtsp/
camera_service.rs

1//! The service: owns every camera, and is the only thing HTTP talks to.
2
3use crate::frame_router::{FrameRouter, Subscription};
4use crate::ports::VideoSourceFactory;
5use dahua_camera_core::ports::{CameraControl, CameraControlFactory, EventSourceFactory};
6use crate::supervisor::{supervise, supervise_link, RestartPolicy, SupervisedLink, SupervisorCtx};
7use dahua_camera_core::{
8    AppConfig, CameraConfig, CameraStats, CameraStatus, CapabilitySet, Event, SharedParams,
9};
10use std::collections::BTreeMap;
11use std::sync::Arc;
12use std::time::Duration;
13use tokio::sync::broadcast;
14use tokio_util::sync::CancellationToken;
15
16/// Depth of the shared analytics-event ring.
17const EVENT_BUS_CAPACITY: usize = 128;
18
19
20/// The adapters a [`CameraService`] runs on.
21///
22/// This is the seam where concrete implementations enter the application
23/// layer. Production wiring lives in the `dahua-camera` facade; tests substitute the
24/// doubles in [`testing`](crate::testing).
25#[derive(Clone)]
26pub struct ServiceDeps {
27    /// Builds video sources. Required.
28    pub source_factory: Arc<dyn VideoSourceFactory>,
29    /// Builds out-of-band control clients. Required.
30    pub control_factory: Arc<dyn CameraControlFactory>,
31    /// Builds analytics-event sources. `None` disables event collection.
32    pub event_factory: Option<Arc<dyn EventSourceFactory>>,
33    /// Builds the decode lane's codecs.
34    #[cfg(feature = "decode")]
35    pub codec_factory: Arc<dyn crate::ports::CodecFactory>,
36    /// Restart timings applied to every camera.
37    pub policy: RestartPolicy,
38}
39
40/// One camera, as the rest of the program sees it.
41///
42/// Holds no I/O of its own: the supervisor writes into the router, consumers
43/// read out of it, and control commands go straight to the adapter.
44pub struct Camera {
45    id: String,
46    config: CameraConfig,
47    stats: Arc<CameraStats>,
48    router: Arc<FrameRouter>,
49    control: Arc<dyn CameraControl>,
50    /// What the device reports it can do.
51    ///
52    /// Starts **unprobed**, so every capability-gated call refuses until a
53    /// probe fills it in. A `RwLock` because reads are frequent (every HTTP
54    /// status poll) and writes happen once, at probe time.
55    capabilities: std::sync::RwLock<CapabilitySet>,
56    #[cfg(feature = "decode")]
57    jpeg: tokio::sync::Mutex<Option<crate::decoder::JpegLane>>,
58    #[cfg(feature = "decode")]
59    codec_factory: Arc<dyn crate::ports::CodecFactory>,
60}
61
62impl Camera {
63    /// Stable identifier.
64    pub fn id(&self) -> &str {
65        &self.id
66    }
67
68    /// This camera's configuration.
69    pub fn config(&self) -> &CameraConfig {
70        &self.config
71    }
72
73    /// Live counters.
74    pub fn stats(&self) -> &CameraStats {
75        &self.stats
76    }
77
78    /// Current lifecycle state.
79    pub fn status(&self) -> CameraStatus {
80        self.router.status()
81    }
82
83    /// Active codec parameters, if a session has published any.
84    pub fn params(&self) -> Option<SharedParams> {
85        self.router.params()
86    }
87
88    /// How many consumers are attached.
89    pub fn viewer_count(&self) -> usize {
90        self.router.viewer_count()
91    }
92
93    /// Out-of-band control for this camera.
94    pub fn control(&self) -> &Arc<dyn CameraControl> {
95        &self.control
96    }
97
98    /// What this camera reports it can do.
99    ///
100    /// Empty and unprobed until something calls
101    /// [`set_capabilities`](Self::set_capabilities). Callers must treat an
102    /// unprobed profile as "refuse", never as "assume yes".
103    pub fn capabilities(&self) -> CapabilitySet {
104        self.capabilities.read().expect("capabilities lock").clone()
105    }
106
107    /// Record the result of a capability probe.
108    pub fn set_capabilities(&self, capabilities: CapabilitySet) {
109        *self.capabilities.write().expect("capabilities lock") = capabilities;
110    }
111
112    /// Attach to the frame bus.
113    ///
114    /// For a camera without `autostart`, this is what causes the supervisor to
115    /// connect. Dropping the returned [`Subscription`] releases the viewer
116    /// slot and eventually lets the session be torn down.
117    pub fn subscribe(&self) -> Subscription {
118        self.router.subscribe(self.stats.clone())
119    }
120
121    /// Wait for codec parameters, subscribing first so a lazy camera starts.
122    ///
123    /// Returns `None` if nothing arrived within `timeout`, which normally
124    /// means the camera is unreachable.
125    pub async fn wait_for_params(&self, timeout: Duration) -> Option<SharedParams> {
126        self.router.wait_for_params(timeout).await
127    }
128}
129
130#[cfg(feature = "decode")]
131impl Camera {
132    /// Get or start this camera's JPEG decode lane.
133    ///
134    /// The lane is created on first use and shared by every MJPEG and snapshot
135    /// consumer, so N viewers cost one decode, not N. It shuts itself down
136    /// once the last handle is dropped.
137    pub async fn jpeg_lane(&self) -> crate::decoder::JpegLane {
138        let mut slot = self.jpeg.lock().await;
139        if let Some(lane) = slot.as_ref() {
140            if lane.is_alive() {
141                return lane.clone();
142            }
143        }
144        let lane = crate::decoder::spawn_lane(
145            &self.id,
146            self.config.jpeg_quality,
147            self.subscribe(),
148            self.stats.clone(),
149            self.codec_factory.clone(),
150        );
151        *slot = Some(lane.clone());
152        lane
153    }
154}
155
156/// Owns every configured camera and their supervisors.
157pub struct CameraService {
158    cameras: BTreeMap<String, Arc<Camera>>,
159    events_tx: broadcast::Sender<Event>,
160    cancel: CancellationToken,
161    /// Long-lived tasks this service spawned, for the cost budget.
162    ///
163    /// Counted at the spawn sites rather than read from the runtime, because
164    /// `RuntimeMetrics::spawned_tasks_count` needs `tokio_unstable`, and
165    /// because this counts exactly what the budget is about — supervised links
166    /// — rather than every transient task on the runtime.
167    spawned_tasks: std::sync::atomic::AtomicUsize,
168}
169
170impl CameraService {
171    /// Build the service and spawn one supervisor per camera.
172    ///
173    /// Returns immediately: cameras with `autostart` begin connecting in the
174    /// background, the rest stay idle until something subscribes.
175    pub fn start(config: &AppConfig, deps: ServiceDeps) -> Arc<Self> {
176        let cancel = CancellationToken::new();
177        let (events_tx, _) = broadcast::channel(EVENT_BUS_CAPACITY);
178        let mut cameras = BTreeMap::new();
179        let mut spawned = 0usize;
180
181        for (id, camera_config) in &config.cameras {
182            let router = Arc::new(FrameRouter::new());
183            let stats = Arc::new(CameraStats::default());
184
185            cameras.insert(
186                id.clone(),
187                Arc::new(Camera {
188                    id: id.clone(),
189                    config: camera_config.clone(),
190                    stats: stats.clone(),
191                    router: router.clone(),
192                    control: deps.control_factory.create(camera_config),
193                    capabilities: std::sync::RwLock::new(CapabilitySet::default()),
194                    #[cfg(feature = "decode")]
195                    jpeg: tokio::sync::Mutex::new(None),
196                    #[cfg(feature = "decode")]
197                    codec_factory: deps.codec_factory.clone(),
198                }),
199            );
200
201            spawned += 1;
202            tokio::spawn(supervise(SupervisorCtx {
203                id: id.clone(),
204                config: camera_config.clone(),
205                router,
206                stats,
207                source_factory: deps.source_factory.clone(),
208                cancel: cancel.child_token(),
209                policy: deps.policy.clone(),
210            }));
211
212            // The event channel is a link like any other: same loop, same
213            // backoff, same panic isolation — and its own child token, so it
214            // reconnects entirely independently of video.
215            if let Some(factory) = &deps.event_factory {
216                let link = Arc::new(EventLink {
217                    source: factory.create(camera_config),
218                    sink: events_tx.clone(),
219                });
220                spawned += 1;
221                tokio::spawn(supervise_link(
222                    id.clone(),
223                    link,
224                    cancel.child_token(),
225                    deps.policy.clone(),
226                ));
227            }
228        }
229
230        Arc::new(Self {
231            cameras,
232            events_tx,
233            cancel,
234            spawned_tasks: std::sync::atomic::AtomicUsize::new(spawned),
235        })
236    }
237
238    /// Look up one camera.
239    pub fn get(&self, id: &str) -> Option<Arc<Camera>> {
240        self.cameras.get(id).cloned()
241    }
242
243    /// Every camera, in stable id order.
244    pub fn all(&self) -> impl Iterator<Item = &Arc<Camera>> {
245        self.cameras.values()
246    }
247
248    /// Attach to the merged analytics-event stream from all cameras.
249    pub fn subscribe_events(&self) -> broadcast::Receiver<Event> {
250        self.events_tx.subscribe()
251    }
252
253    /// How many long-lived tasks this service spawned.
254    ///
255    /// One supervised link per task. The workspace budget allows **no feature
256    /// more than one task per camera**, and `tests/task_budget.rs` asserts it
257    /// against this number.
258    pub fn spawned_task_count(&self) -> usize {
259        self.spawned_tasks.load(std::sync::atomic::Ordering::Relaxed)
260    }
261
262    /// Stop every camera. Supervisors wind down asynchronously.
263    pub fn shutdown(&self) {
264        self.cancel.cancel();
265    }
266}
267
268/// One camera's analytics event channel, as a [`SupervisedLink`].
269///
270/// This exists instead of a bespoke retry loop. The previous implementation
271/// was a bare `tokio::spawn` with a flat ten-second retry: no exponential
272/// backoff, no distinction between a dropped connection and rejected
273/// credentials, and — because nothing awaited its `JoinHandle` — a panic in
274/// the parser killed event reporting for that camera silently, for the life of
275/// the process.
276struct EventLink {
277    source: Arc<dyn dahua_camera_core::ports::EventSource>,
278    sink: broadcast::Sender<Event>,
279}
280
281#[async_trait::async_trait]
282impl SupervisedLink for EventLink {
283    fn name(&self) -> &'static str {
284        "events"
285    }
286
287    async fn run_once(&self, cancel: CancellationToken) -> dahua_camera_core::Result<()> {
288        let mut stream = self.source.connect().await?;
289        tracing::info!("event stream connected");
290
291        loop {
292            let next = tokio::select! {
293                biased;
294                _ = cancel.cancelled() => return Ok(()),
295                r = stream.next() => r,
296            };
297
298            match next? {
299                // A send error means nothing is subscribed to the event bus.
300                // That is not a fault: events are advisory, and a service with
301                // no event consumers should not be reconnecting over it.
302                Some(event) => {
303                    let _ = self.sink.send(event);
304                }
305                None => {
306                    tracing::debug!("event stream closed by peer");
307                    return Err(dahua_camera_core::CameraError::Cgi {
308                        camera_id: String::new(),
309                        detail: "event stream closed".to_owned(),
310                    });
311                }
312            }
313        }
314    }
315}