use std::process::Stdio;
use std::time::Duration;
use regex::Regex;
use tokio::process::Command;
use tracing::{debug, warn};
use crate::error::ProbeError;
pub async fn profile_bitrate(
url: &str,
duration_secs: u64,
timeout_secs: u64,
) -> Result<u64, ProbeError> {
let duration_str = duration_secs.to_string();
let mut cmd = Command::new("ffmpeg");
cmd.args([
"-v",
"debug",
"-user_agent",
"VLC/3.0.14",
"-i",
url,
"-t",
&duration_str,
"-f",
"null",
"-",
])
.stdout(Stdio::null())
.stderr(Stdio::piped())
.stdin(Stdio::null());
debug!(url, duration_secs, "profiling bitrate");
let child = cmd.spawn().map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
ProbeError::FfmpegNotFound
} else {
ProbeError::Io(e)
}
})?;
let result = tokio::time::timeout(Duration::from_secs(timeout_secs), child.wait_with_output())
.await
.map_err(|_| ProbeError::Timeout {
url: url.to_string(),
timeout_secs,
})?
.map_err(ProbeError::Io)?;
let stderr = String::from_utf8_lossy(&result.stderr);
let bytes_re = Regex::new(r"(\d+)\s+bytes\s+read").expect("valid regex");
let total_bytes: u64 = stderr
.lines()
.filter(|line| line.contains("Statistics:") && line.contains("bytes read"))
.filter_map(|line| {
bytes_re
.captures(line)
.and_then(|caps| caps.get(1))
.and_then(|m| m.as_str().parse::<u64>().ok())
})
.next_back()
.unwrap_or(0);
if total_bytes == 0 {
warn!(url, "no bytes-read statistics found in ffmpeg output");
return Err(ProbeError::ProcessFailed {
code: result.status.code(),
stderr: "no bytes-read statistics in ffmpeg debug output".to_string(),
});
}
let bitrate_bps = (total_bytes * 8) / duration_secs;
debug!(url, bitrate_bps, total_bytes, "bitrate profiled");
Ok(bitrate_bps)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bytes_read_regex_matches() {
let re = Regex::new(r"(\d+)\s+bytes\s+read").unwrap();
let line = " Statistics: 1234567 bytes read, 0 seeks";
let caps = re.captures(line).unwrap();
assert_eq!(caps.get(1).unwrap().as_str(), "1234567");
}
#[test]
fn bytes_read_regex_no_match() {
let re = Regex::new(r"(\d+)\s+bytes\s+read").unwrap();
assert!(re.captures("no stats here").is_none());
}
}