use crate::error::DecodeError;
use ff_sys::error_codes;
const URL_SCHEMES: &[&str] = &[
"http://", "https://", "rtmp://", "rtsp://", "udp://", "srt://", "rtp://",
];
pub(crate) fn is_url(path: &str) -> bool {
URL_SCHEMES.iter().any(|scheme| path.starts_with(scheme))
}
pub(crate) fn sanitize_url(url: &str) -> String {
let Some(scheme_end) = url.find("://") else {
return url.to_owned();
};
let after_scheme = scheme_end + 3;
let authority_end = url[after_scheme..]
.find('/')
.map_or(url.len(), |i| after_scheme + i);
let authority = &url[after_scheme..authority_end];
let scheme_prefix = &url[..after_scheme]; let path_part = &url[authority_end..];
let path_clean = path_part.find('?').map_or(path_part, |i| &path_part[..i]);
if let Some(at) = authority.rfind('@') {
let user_info = &authority[..at];
let host = &authority[at + 1..];
let safe_user_info = user_info.find(':').map_or_else(
|| user_info.to_owned(),
|colon| format!("{}:***", &user_info[..colon]),
);
format!("{scheme_prefix}{safe_user_info}@{host}{path_clean}")
} else {
format!("{scheme_prefix}{authority}{path_clean}")
}
}
pub(crate) fn check_srt_url(url: &str) -> Result<(), DecodeError> {
if !url.starts_with("srt://") {
return Ok(());
}
check_srt_available(url)
}
#[cfg(not(feature = "srt"))]
fn check_srt_available(url: &str) -> Result<(), DecodeError> {
Err(DecodeError::ConnectionFailed {
code: 0,
endpoint: sanitize_url(url),
message: "SRT protocol is not enabled; recompile with feature = \"srt\"".to_string(),
})
}
#[cfg(feature = "srt")]
fn check_srt_available(url: &str) -> Result<(), DecodeError> {
if !ff_sys::avformat::srt_available() {
return Err(DecodeError::ConnectionFailed {
code: 0,
endpoint: sanitize_url(url),
message: "SRT protocol is not available in the linked FFmpeg build; \
recompile FFmpeg with --enable-libsrt"
.to_string(),
});
}
Ok(())
}
pub(crate) fn map_network_error(code: i32, endpoint: String) -> DecodeError {
let message = ff_sys::av_error_string(code);
match code {
c if c == error_codes::ETIMEDOUT => DecodeError::NetworkTimeout {
code,
endpoint,
message,
},
c if c == error_codes::ECONNREFUSED
|| c == error_codes::EHOSTUNREACH
|| c == error_codes::ENETUNREACH =>
{
DecodeError::ConnectionFailed {
code,
endpoint,
message,
}
}
c if c == error_codes::EIO => DecodeError::StreamInterrupted {
code,
endpoint,
message,
},
_ => DecodeError::Ffmpeg { code, message },
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_url_should_return_true_for_http() {
assert!(is_url("http://example.com/stream"));
}
#[test]
fn is_url_should_return_true_for_https() {
assert!(is_url("https://cdn.example.com/index.m3u8"));
}
#[test]
fn is_url_should_return_true_for_rtmp() {
assert!(is_url("rtmp://live.example.com/app/key"));
}
#[test]
fn is_url_should_return_true_for_rtsp() {
assert!(is_url("rtsp://camera.local/stream"));
}
#[test]
fn is_url_should_return_true_for_udp() {
assert!(is_url("udp://239.0.0.1:1234"));
}
#[test]
fn is_url_should_return_true_for_srt() {
assert!(is_url("srt://ingest.example.com:4200"));
}
#[test]
fn is_url_should_return_true_for_rtp() {
assert!(is_url("rtp://239.0.0.1:5004"));
}
#[test]
fn is_url_should_return_false_for_local_path() {
assert!(!is_url("/home/user/video.mp4"));
}
#[test]
fn is_url_should_return_false_for_relative_path() {
assert!(!is_url("video.mp4"));
}
#[test]
fn is_url_should_return_false_for_windows_path() {
assert!(!is_url("C:/Users/user/video.mp4"));
}
#[test]
fn sanitize_url_should_strip_password_from_rtmp_url() {
assert_eq!(
sanitize_url("rtmp://admin:s3cr3t@live.example.com/app/key"),
"rtmp://admin:***@live.example.com/app/key",
);
}
#[test]
fn sanitize_url_should_strip_password_and_query_string() {
assert_eq!(
sanitize_url("rtmp://admin:s3cr3t@live.example.com/app/key?token=abc"),
"rtmp://admin:***@live.example.com/app/key",
);
}
#[test]
fn sanitize_url_should_strip_query_string_without_credentials() {
assert_eq!(
sanitize_url("http://cdn.example.com/live.m3u8?token=secret"),
"http://cdn.example.com/live.m3u8",
);
}
#[test]
fn sanitize_url_should_leave_url_without_credentials_unchanged() {
assert_eq!(
sanitize_url("rtmp://live.example.com/app/key"),
"rtmp://live.example.com/app/key",
);
}
#[test]
fn sanitize_url_should_handle_username_without_password() {
assert_eq!(
sanitize_url("rtmp://admin@live.example.com/app/key"),
"rtmp://admin@live.example.com/app/key",
);
}
#[test]
fn sanitize_url_should_return_input_when_no_scheme_found() {
let raw = "not-a-url";
assert_eq!(sanitize_url(raw), raw);
}
#[test]
fn map_network_error_should_map_etimedout_to_network_timeout() {
let err = map_network_error(error_codes::ETIMEDOUT, "rtmp://host/app".to_string());
assert!(matches!(err, DecodeError::NetworkTimeout { .. }));
}
#[test]
fn map_network_error_should_map_econnrefused_to_connection_failed() {
let err = map_network_error(error_codes::ECONNREFUSED, "rtmp://host/app".to_string());
assert!(matches!(err, DecodeError::ConnectionFailed { .. }));
}
#[test]
fn map_network_error_should_map_ehostunreach_to_connection_failed() {
let err = map_network_error(error_codes::EHOSTUNREACH, "rtmp://host/app".to_string());
assert!(matches!(err, DecodeError::ConnectionFailed { .. }));
}
#[test]
fn map_network_error_should_map_enetunreach_to_connection_failed() {
let err = map_network_error(error_codes::ENETUNREACH, "rtmp://host/app".to_string());
assert!(matches!(err, DecodeError::ConnectionFailed { .. }));
}
#[test]
fn map_network_error_should_map_eio_to_stream_interrupted() {
let err = map_network_error(error_codes::EIO, "rtmp://host/app".to_string());
assert!(matches!(err, DecodeError::StreamInterrupted { .. }));
}
#[test]
fn map_network_error_should_map_unknown_code_to_ffmpeg() {
let err = map_network_error(-22, "rtmp://host/app".to_string());
assert!(matches!(err, DecodeError::Ffmpeg { .. }));
}
#[test]
fn map_network_error_should_include_sanitized_endpoint_in_connection_failed() {
let endpoint = "rtmp://admin:***@host/app".to_string();
let err = map_network_error(error_codes::ECONNREFUSED, endpoint.clone());
match err {
DecodeError::ConnectionFailed { endpoint: ep, .. } => assert_eq!(ep, endpoint),
_ => panic!("wrong variant"),
}
}
}