Skip to main content

crispy_iptv_types/
stream.rs

1//! Stream URL types and validation.
2
3use serde::{Deserialize, Serialize};
4
5/// A validated stream URL with detected protocol.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct StreamUrl {
8    /// The raw URL string.
9    pub url: String,
10    /// Detected protocol.
11    pub protocol: StreamProtocol,
12}
13
14impl StreamUrl {
15    /// Parse a URL string and detect its streaming protocol.
16    pub fn parse(url: &str) -> Self {
17        let protocol = StreamProtocol::detect(url);
18        Self {
19            url: url.to_string(),
20            protocol,
21        }
22    }
23}
24
25/// Detected streaming protocol.
26#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub enum StreamProtocol {
28    Http,
29    Https,
30    Hls,
31    Dash,
32    Rtmp,
33    Rtsp,
34    Udp,
35    Rtp,
36    Mms,
37    #[default]
38    Unknown,
39}
40
41impl StreamProtocol {
42    /// Detect protocol from a URL string.
43    pub fn detect(url: &str) -> Self {
44        let lower = url.to_ascii_lowercase();
45        if lower.starts_with("rtmp://") || lower.starts_with("rtmps://") {
46            return Self::Rtmp;
47        }
48        if lower.starts_with("rtsp://") {
49            return Self::Rtsp;
50        }
51        if lower.starts_with("udp://") {
52            return Self::Udp;
53        }
54        if lower.starts_with("rtp://") {
55            return Self::Rtp;
56        }
57        if lower.starts_with("mms://") || lower.starts_with("mmsh://") {
58            return Self::Mms;
59        }
60        // HLS / DASH detection by extension.
61        if lower.contains(".m3u8") || lower.contains("/hls/") {
62            return Self::Hls;
63        }
64        if lower.contains(".mpd") || lower.contains("/dash/") {
65            return Self::Dash;
66        }
67        if lower.starts_with("https://") {
68            return Self::Https;
69        }
70        if lower.starts_with("http://") {
71            return Self::Http;
72        }
73        Self::Unknown
74    }
75}
76
77/// Result of checking a stream's availability.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct StreamStatus {
80    /// Whether the stream is reachable.
81    pub available: bool,
82    /// HTTP status code (if applicable).
83    pub status_code: Option<u16>,
84    /// Response time in milliseconds.
85    pub response_time_ms: Option<u64>,
86    /// Detected content type.
87    pub content_type: Option<String>,
88    /// Error message if unavailable.
89    pub error: Option<String>,
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn detect_hls() {
98        assert_eq!(
99            StreamProtocol::detect("http://example.com/live/stream.m3u8"),
100            StreamProtocol::Hls,
101        );
102    }
103
104    #[test]
105    fn detect_rtmp() {
106        assert_eq!(
107            StreamProtocol::detect("rtmp://cdn.example.com/live/key"),
108            StreamProtocol::Rtmp,
109        );
110    }
111
112    #[test]
113    fn detect_http() {
114        assert_eq!(
115            StreamProtocol::detect("http://example.com/stream.ts"),
116            StreamProtocol::Http,
117        );
118    }
119
120    #[test]
121    fn detect_udp() {
122        assert_eq!(
123            StreamProtocol::detect("udp://239.0.0.1:5000"),
124            StreamProtocol::Udp,
125        );
126    }
127
128    #[test]
129    fn detect_dash() {
130        assert_eq!(
131            StreamProtocol::detect("https://cdn.example.com/manifest.mpd"),
132            StreamProtocol::Dash,
133        );
134    }
135
136    #[test]
137    fn detect_unknown() {
138        assert_eq!(
139            StreamProtocol::detect("ftp://example.com/file"),
140            StreamProtocol::Unknown,
141        );
142    }
143
144    #[test]
145    fn stream_url_parse() {
146        let s = StreamUrl::parse("https://cdn.example.com/live/stream.m3u8");
147        assert_eq!(s.protocol, StreamProtocol::Hls);
148    }
149}