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}