Skip to main content

nv_core/
config.rs

1//! Configuration types shared across crates.
2//!
3//! Source specifications, camera modes, reconnection policies, and
4//! backoff strategies live here so downstream crates can reference
5//! them without depending on the media or runtime crates.
6
7use std::fmt;
8use std::path::PathBuf;
9
10use crate::security::{RtspSecurityPolicy, redact_url};
11
12/// Specification of a video source.
13///
14/// Defined in `nv-core` (not `nv-media`) to prevent downstream crates
15/// from transitively depending on GStreamer.
16#[derive(Clone)]
17pub enum SourceSpec {
18    /// An RTSP stream.
19    ///
20    /// The `security` field controls TLS enforcement. The default
21    /// ([`PreferTls`](RtspSecurityPolicy::PreferTls)) promotes bare
22    /// `rtsp://` URLs to `rtsps://` at pipeline construction time.
23    Rtsp {
24        url: String,
25        transport: RtspTransport,
26        /// TLS security policy for this source. Default: `PreferTls`.
27        security: RtspSecurityPolicy,
28    },
29
30    /// A local video file.
31    File {
32        path: PathBuf,
33        /// Whether to loop the file when it reaches the end.
34        loop_: bool,
35    },
36
37    /// A Video4Linux2 device (Linux only).
38    V4l2 { device: String },
39
40    /// Escape hatch: a raw pipeline launch-line fragment.
41    ///
42    /// The library constructs the pipeline internally for all other variants.
43    /// Use this only for exotic sources not covered above.
44    ///
45    /// **Security note:** Custom pipelines are gated by
46    /// [`CustomPipelinePolicy`](crate::security::CustomPipelinePolicy) at
47    /// the runtime layer. The default policy rejects custom pipelines;
48    /// set `CustomPipelinePolicy::AllowTrusted` on the runtime builder to
49    /// enable them.
50    Custom { pipeline_fragment: String },
51}
52
53impl SourceSpec {
54    /// Convenience constructor for an RTSP source with TCP transport.
55    ///
56    /// The security policy is inferred from the URL scheme:
57    ///
58    /// - `rtsps://` or no recognized scheme → [`PreferTls`](RtspSecurityPolicy::PreferTls)
59    /// - `rtsp://` → [`AllowInsecure`](RtspSecurityPolicy::AllowInsecure)
60    ///
61    /// An explicit `rtsp://` scheme is treated as a deliberate choice by
62    /// the caller and is **not** promoted to `rtsps://`. Use
63    /// [`rtsp_tls`](Self::rtsp_tls) to force TLS promotion on a bare
64    /// `rtsp://` URL.
65    #[must_use]
66    pub fn rtsp(url: impl Into<String>) -> Self {
67        let url = url.into();
68        // Explicit rtsp:// means the caller chose plaintext.
69        let security = if url.starts_with("rtsp://") {
70            RtspSecurityPolicy::AllowInsecure
71        } else {
72            // rtsps://, scheme-less, or unknown → try TLS.
73            RtspSecurityPolicy::PreferTls
74        };
75        Self::Rtsp {
76            url,
77            transport: RtspTransport::Tcp,
78            security,
79        }
80    }
81
82    /// Convenience constructor that forces [`PreferTls`](RtspSecurityPolicy::PreferTls).
83    ///
84    /// Unlike [`rtsp()`](Self::rtsp), this promotes bare `rtsp://` URLs to
85    /// `rtsps://` at pipeline construction time. Use this when you know
86    /// the camera supports TLS but the URL was provided without the
87    /// `rtsps://` scheme.
88    #[must_use]
89    pub fn rtsp_tls(url: impl Into<String>) -> Self {
90        Self::Rtsp {
91            url: url.into(),
92            transport: RtspTransport::Tcp,
93            security: RtspSecurityPolicy::PreferTls,
94        }
95    }
96
97    /// Convenience constructor for an RTSP source with explicit insecure
98    /// transport ([`AllowInsecure`](RtspSecurityPolicy::AllowInsecure)).
99    ///
100    /// Equivalent to [`rtsp()`](Self::rtsp) for `rtsp://` URLs. Useful
101    /// when constructing a spec from a variable where you want to
102    /// guarantee `AllowInsecure` regardless of the scheme.
103    #[must_use]
104    pub fn rtsp_insecure(url: impl Into<String>) -> Self {
105        Self::Rtsp {
106            url: url.into(),
107            transport: RtspTransport::Tcp,
108            security: RtspSecurityPolicy::AllowInsecure,
109        }
110    }
111
112    /// Convenience constructor for a local video file (non-looping).
113    #[must_use]
114    pub fn file(path: impl Into<PathBuf>) -> Self {
115        Self::File {
116            path: path.into(),
117            loop_: false,
118        }
119    }
120
121    /// Convenience constructor for a looping local video file.
122    ///
123    /// The source seeks back to the start on EOS instead of stopping.
124    #[must_use]
125    pub fn file_looping(path: impl Into<PathBuf>) -> Self {
126        Self::File {
127            path: path.into(),
128            loop_: true,
129        }
130    }
131
132    /// Returns `true` if this is a non-looping file source.
133    ///
134    /// Non-looping file sources treat EOS as terminal (not an error):
135    /// the feed stops with [`StopReason::EndOfStream`](crate::health::StopReason::EndOfStream)
136    /// rather than attempting a restart.
137    #[must_use]
138    pub fn is_file_nonloop(&self) -> bool {
139        matches!(self, Self::File { loop_: false, .. })
140    }
141}
142
143impl fmt::Debug for SourceSpec {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self {
146            Self::Rtsp {
147                url,
148                transport,
149                security,
150            } => f
151                .debug_struct("Rtsp")
152                .field("url", &redact_url(url))
153                .field("transport", transport)
154                .field("security", security)
155                .finish(),
156            Self::File { path, loop_ } => f
157                .debug_struct("File")
158                .field("path", path)
159                .field("loop_", loop_)
160                .finish(),
161            Self::V4l2 { device } => f.debug_struct("V4l2").field("device", device).finish(),
162            Self::Custom { .. } => f
163                .debug_struct("Custom")
164                .field("pipeline_fragment", &"<redacted>")
165                .finish(),
166        }
167    }
168}
169
170/// RTSP transport protocol.
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub enum RtspTransport {
173    /// TCP interleaved — more reliable over lossy networks.
174    Tcp,
175    /// UDP unicast — lower latency, may lose packets.
176    UdpUnicast,
177}
178
179/// Declared camera installation mode.
180///
181/// This is a **required** field on feed configuration — there is no default.
182/// The mode determines whether the view system is engaged.
183///
184/// See the architecture docs §9 for full details on why this is required.
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186pub enum CameraMode {
187    /// Camera is physically fixed (bolted mount, no PTZ, no gimbal).
188    ///
189    /// The view system is bypassed entirely. No `ViewStateProvider` is needed.
190    /// `CameraMotionState` is always `Stable`, `MotionSource` is always `None`.
191    Fixed,
192
193    /// Camera may move (PTZ, gimbal, handheld, vehicle-mounted, drone, etc.).
194    ///
195    /// A `ViewStateProvider` is **required**. If the provider returns no data,
196    /// the view system defaults to `CameraMotionState::Unknown` and
197    /// `ContextValidity::Degraded` — never to `Stable`.
198    Observed,
199}
200
201/// Reconnection policy for video sources.
202#[derive(Debug, Clone)]
203pub struct ReconnectPolicy {
204    /// Maximum reconnection attempts. `0` = infinite retries.
205    pub max_attempts: u32,
206    /// Base delay between reconnection attempts.
207    pub base_delay: std::time::Duration,
208    /// Maximum delay (caps exponential backoff).
209    pub max_delay: std::time::Duration,
210    /// Backoff strategy.
211    pub backoff: BackoffKind,
212}
213
214impl Default for ReconnectPolicy {
215    fn default() -> Self {
216        Self {
217            max_attempts: 0,
218            base_delay: std::time::Duration::from_secs(1),
219            max_delay: std::time::Duration::from_secs(30),
220            backoff: BackoffKind::Exponential,
221        }
222    }
223}
224
225/// Backoff strategy for reconnection attempts.
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub enum BackoffKind {
228    /// Delay doubles each attempt (capped at `max_delay`).
229    Exponential,
230    /// Delay increases linearly by `base_delay` each attempt.
231    Linear,
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use crate::security::RtspSecurityPolicy;
238
239    #[test]
240    fn file_looping_creates_looping_file_spec() {
241        let spec = SourceSpec::file_looping("/tmp/test.mp4");
242        match &spec {
243            SourceSpec::File { path, loop_ } => {
244                assert_eq!(path.to_str().unwrap(), "/tmp/test.mp4");
245                assert!(*loop_, "file_looping should create a looping spec");
246            }
247            _ => panic!("expected File variant"),
248        }
249        assert!(!spec.is_file_nonloop());
250    }
251
252    #[test]
253    fn file_creates_nonlooping_file_spec() {
254        let spec = SourceSpec::file("/tmp/test.mp4");
255        assert!(spec.is_file_nonloop());
256    }
257
258    #[test]
259    fn rtsp_plain_scheme_infers_allow_insecure() {
260        let spec = SourceSpec::rtsp("rtsp://example.com/stream");
261        match &spec {
262            SourceSpec::Rtsp {
263                url,
264                transport,
265                security,
266            } => {
267                assert_eq!(url, "rtsp://example.com/stream");
268                assert_eq!(*transport, RtspTransport::Tcp);
269                assert_eq!(*security, RtspSecurityPolicy::AllowInsecure);
270            }
271            _ => panic!("expected Rtsp variant"),
272        }
273    }
274
275    #[test]
276    fn rtsp_tls_scheme_infers_prefer_tls() {
277        let spec = SourceSpec::rtsp("rtsps://example.com/stream");
278        match &spec {
279            SourceSpec::Rtsp {
280                url,
281                transport,
282                security,
283            } => {
284                assert_eq!(url, "rtsps://example.com/stream");
285                assert_eq!(*transport, RtspTransport::Tcp);
286                assert_eq!(*security, RtspSecurityPolicy::PreferTls);
287            }
288            _ => panic!("expected Rtsp variant"),
289        }
290    }
291
292    #[test]
293    fn rtsp_no_scheme_infers_prefer_tls() {
294        let spec = SourceSpec::rtsp("example.com/stream");
295        match &spec {
296            SourceSpec::Rtsp { security, .. } => {
297                assert_eq!(*security, RtspSecurityPolicy::PreferTls);
298            }
299            _ => panic!("expected Rtsp variant"),
300        }
301    }
302
303    #[test]
304    fn rtsp_tls_forces_prefer_tls() {
305        let spec = SourceSpec::rtsp_tls("rtsp://example.com/stream");
306        match &spec {
307            SourceSpec::Rtsp { url, security, .. } => {
308                assert_eq!(url, "rtsp://example.com/stream");
309                assert_eq!(*security, RtspSecurityPolicy::PreferTls);
310            }
311            _ => panic!("expected Rtsp variant"),
312        }
313    }
314
315    #[test]
316    fn rtsp_insecure_creates_allow_insecure_spec() {
317        let spec = SourceSpec::rtsp_insecure("rtsp://example.com/stream");
318        match &spec {
319            SourceSpec::Rtsp { url, security, .. } => {
320                assert_eq!(url, "rtsp://example.com/stream");
321                assert_eq!(*security, RtspSecurityPolicy::AllowInsecure);
322            }
323            _ => panic!("expected Rtsp variant"),
324        }
325    }
326}