#![allow(dead_code)]
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ProtocolType {
Hls,
Dash,
Rtmp,
Srt,
WebRtc,
Smpte2110,
Rtp,
Unknown,
}
impl fmt::Display for ProtocolType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::Hls => "HLS",
Self::Dash => "DASH",
Self::Rtmp => "RTMP",
Self::Srt => "SRT",
Self::WebRtc => "WebRTC",
Self::Smpte2110 => "SMPTE2110",
Self::Rtp => "RTP",
Self::Unknown => "Unknown",
};
f.write_str(label)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DetectionResult {
pub protocol: ProtocolType,
pub confidence: f64,
pub reason: String,
}
impl DetectionResult {
#[allow(clippy::cast_precision_loss)]
pub fn new(protocol: ProtocolType, confidence: f64, reason: impl Into<String>) -> Self {
Self {
protocol,
confidence: confidence.clamp(0.0, 1.0),
reason: reason.into(),
}
}
pub fn is_confident(&self, threshold: f64) -> bool {
self.confidence >= threshold
}
}
pub fn detect_from_uri(uri: &str) -> DetectionResult {
let lower = uri.to_ascii_lowercase();
if lower.starts_with("rtmp://") || lower.starts_with("rtmps://") {
return DetectionResult::new(ProtocolType::Rtmp, 1.0, "URI scheme rtmp(s)");
}
if lower.starts_with("srt://") {
return DetectionResult::new(ProtocolType::Srt, 1.0, "URI scheme srt");
}
if lower.starts_with("rtp://") || lower.starts_with("rtsp://") {
return DetectionResult::new(ProtocolType::Rtp, 0.9, "URI scheme rtp/rtsp");
}
if lower.starts_with("http://") || lower.starts_with("https://") {
if lower.ends_with(".m3u8") || lower.contains("/hls/") {
return DetectionResult::new(
ProtocolType::Hls,
0.95,
"HTTP URI with .m3u8 or /hls/ path",
);
}
if lower.ends_with(".mpd") || lower.contains("/dash/") {
return DetectionResult::new(
ProtocolType::Dash,
0.95,
"HTTP URI with .mpd or /dash/ path",
);
}
return DetectionResult::new(
ProtocolType::Unknown,
0.1,
"HTTP URI with no clear indicator",
);
}
DetectionResult::new(ProtocolType::Unknown, 0.0, "unrecognised URI scheme")
}
const RTMP_MAGIC: u8 = 0x03;
pub const MIN_PROBE_BYTES: usize = 4;
pub fn detect_from_bytes(data: &[u8]) -> DetectionResult {
if data.is_empty() {
return DetectionResult::new(ProtocolType::Unknown, 0.0, "empty data");
}
if data[0] == RTMP_MAGIC && data.len() >= 4 {
return DetectionResult::new(ProtocolType::Rtmp, 0.85, "C0 magic byte 0x03");
}
if data.len() >= 4 && data[0] == 0x00 && data[1] == 0x01 {
return DetectionResult::new(ProtocolType::WebRtc, 0.7, "STUN binding request header");
}
if data[0] >> 6 == 2 && data.len() >= 4 {
return DetectionResult::new(ProtocolType::Rtp, 0.6, "RTP version 2 bit pattern");
}
DetectionResult::new(ProtocolType::Unknown, 0.0, "no matching byte signature")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DefaultPort {
pub protocol: ProtocolType,
pub port: u16,
pub tls: bool,
}
pub fn default_ports() -> Vec<DefaultPort> {
vec![
DefaultPort {
protocol: ProtocolType::Rtmp,
port: 1935,
tls: false,
},
DefaultPort {
protocol: ProtocolType::Srt,
port: 9000,
tls: false,
},
DefaultPort {
protocol: ProtocolType::Hls,
port: 443,
tls: true,
},
DefaultPort {
protocol: ProtocolType::Dash,
port: 443,
tls: true,
},
DefaultPort {
protocol: ProtocolType::Rtp,
port: 5004,
tls: false,
},
DefaultPort {
protocol: ProtocolType::WebRtc,
port: 3478,
tls: false,
},
]
}
pub fn detect_from_port(port: u16) -> DetectionResult {
match port {
1935 => DetectionResult::new(ProtocolType::Rtmp, 0.8, "port 1935 (RTMP)"),
9000 => DetectionResult::new(ProtocolType::Srt, 0.5, "port 9000 (SRT common)"),
5004 => DetectionResult::new(ProtocolType::Rtp, 0.5, "port 5004 (RTP default)"),
3478 | 5349 => DetectionResult::new(ProtocolType::WebRtc, 0.5, "STUN/TURN port"),
80 | 8080 => {
DetectionResult::new(ProtocolType::Unknown, 0.1, "HTTP port, protocol ambiguous")
}
443 | 8443 => {
DetectionResult::new(ProtocolType::Unknown, 0.1, "HTTPS port, protocol ambiguous")
}
_ => DetectionResult::new(ProtocolType::Unknown, 0.0, "unknown port"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_uri_rtmp() {
let r = detect_from_uri("rtmp://live.example.com/app/stream");
assert_eq!(r.protocol, ProtocolType::Rtmp);
assert!((r.confidence - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_detect_uri_srt() {
let r = detect_from_uri("srt://host:9000?mode=caller");
assert_eq!(r.protocol, ProtocolType::Srt);
}
#[test]
fn test_detect_uri_hls_m3u8() {
let r = detect_from_uri("https://cdn.example.com/live/index.m3u8");
assert_eq!(r.protocol, ProtocolType::Hls);
assert!(r.confidence > 0.9);
}
#[test]
fn test_detect_uri_dash_mpd() {
let r = detect_from_uri("https://cdn.example.com/vod/manifest.mpd");
assert_eq!(r.protocol, ProtocolType::Dash);
}
#[test]
fn test_detect_uri_hls_path() {
let r = detect_from_uri("https://cdn.example.com/hls/live");
assert_eq!(r.protocol, ProtocolType::Hls);
}
#[test]
fn test_detect_uri_dash_path() {
let r = detect_from_uri("https://cdn.example.com/dash/live");
assert_eq!(r.protocol, ProtocolType::Dash);
}
#[test]
fn test_detect_uri_unknown() {
let r = detect_from_uri("ftp://files.example.com/video.mp4");
assert_eq!(r.protocol, ProtocolType::Unknown);
}
#[test]
fn test_detect_bytes_rtmp() {
let data = [0x03, 0x00, 0x00, 0x00];
let r = detect_from_bytes(&data);
assert_eq!(r.protocol, ProtocolType::Rtmp);
}
#[test]
fn test_detect_bytes_stun() {
let data = [0x00, 0x01, 0x00, 0x58];
let r = detect_from_bytes(&data);
assert_eq!(r.protocol, ProtocolType::WebRtc);
}
#[test]
fn test_detect_bytes_rtp() {
let data = [0x80, 0x60, 0x00, 0x01];
let r = detect_from_bytes(&data);
assert_eq!(r.protocol, ProtocolType::Rtp);
}
#[test]
fn test_detect_bytes_empty() {
let r = detect_from_bytes(&[]);
assert_eq!(r.protocol, ProtocolType::Unknown);
}
#[test]
fn test_detect_port_rtmp() {
let r = detect_from_port(1935);
assert_eq!(r.protocol, ProtocolType::Rtmp);
assert!(r.confidence > 0.5);
}
#[test]
fn test_confidence_clamped() {
let r = DetectionResult::new(ProtocolType::Hls, 1.5, "over");
assert!((r.confidence - 1.0).abs() < f64::EPSILON);
let r2 = DetectionResult::new(ProtocolType::Hls, -0.5, "under");
assert!((r2.confidence).abs() < f64::EPSILON);
}
#[test]
fn test_protocol_display() {
assert_eq!(format!("{}", ProtocolType::Hls), "HLS");
assert_eq!(format!("{}", ProtocolType::Unknown), "Unknown");
}
#[test]
fn test_default_ports_count() {
let ports = default_ports();
assert!(ports.len() >= 5);
}
#[test]
fn test_is_confident() {
let r = DetectionResult::new(ProtocolType::Srt, 0.75, "test");
assert!(r.is_confident(0.5));
assert!(!r.is_confident(0.9));
}
}