use std::time::{Duration, Instant};
use tracing::{debug, warn};
use crate::error::ProbeError;
use crate::mpv_ffi::{MpvEventId, MpvHandle};
pub async fn capture_screenshot_mpv(
url: &str,
output_path: &str,
seek_secs: Option<f64>,
timeout_secs: u64,
) -> Result<(), ProbeError> {
let url = url.to_string();
let output_path = output_path.to_string();
tokio::task::spawn_blocking(move || {
capture_screenshot_mpv_blocking(&url, &output_path, seek_secs, timeout_secs)
})
.await
.map_err(|e| ProbeError::MpvCommandFailed {
command: "spawn_blocking".to_string(),
detail: e.to_string(),
})?
}
fn capture_screenshot_mpv_blocking(
url: &str,
output_path: &str,
seek_secs: Option<f64>,
timeout_secs: u64,
) -> Result<(), ProbeError> {
debug!(
url,
output_path,
?seek_secs,
"capturing screenshot via libmpv"
);
let handle = MpvHandle::new_for_screenshot()?;
handle.command(&["loadfile", url])?;
let deadline = Instant::now() + Duration::from_secs(timeout_secs);
wait_for_file_loaded(&handle, url, &deadline)?;
if let Some(secs) = seek_secs {
let seek_str = format!("{secs:.1}");
handle.command(&["seek", &seek_str, "absolute"])?;
std::thread::sleep(Duration::from_millis(500));
}
handle.command(&["set", "pause", "no"])?;
std::thread::sleep(Duration::from_millis(200));
handle.command(&["set", "pause", "yes"])?;
handle.command(&["screenshot-to-file", output_path, "video"])?;
debug!(output_path, "mpv screenshot saved");
Ok(())
}
fn wait_for_file_loaded(
handle: &MpvHandle,
url: &str,
deadline: &Instant,
) -> Result<(), ProbeError> {
loop {
let remaining = deadline
.checked_duration_since(Instant::now())
.unwrap_or(Duration::ZERO);
if remaining.is_zero() {
return Err(ProbeError::Timeout {
url: url.to_string(),
timeout_secs: 0, });
}
let wait_secs = remaining.as_secs_f64().min(1.0);
let (event_id, error) = handle.wait_event(wait_secs);
match event_id {
MpvEventId::FileLoaded => {
debug!(url, "mpv: file loaded for screenshot");
return Ok(());
}
MpvEventId::EndFile => {
if error != 0 {
return Err(ProbeError::MpvCommandFailed {
command: format!("loadfile {url}"),
detail: format!("end-file with error code {error}"),
});
}
warn!(url, "mpv: end-file without file-loaded during screenshot");
return Err(ProbeError::NoStreams(url.to_string()));
}
MpvEventId::Shutdown => {
return Err(ProbeError::MpvCommandFailed {
command: format!("loadfile {url}"),
detail: "mpv shutdown during load".to_string(),
});
}
_ => {
}
}
}
}
pub fn is_mpv_screenshot_available() -> bool {
crate::mpv_ffi::is_mpv_available()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_mpv_screenshot_available_does_not_panic() {
let _available = is_mpv_screenshot_available();
}
#[tokio::test]
async fn capture_screenshot_mpv_returns_error_when_no_libmpv() {
let result =
capture_screenshot_mpv("http://invalid.test/stream", "/tmp/test.png", None, 2).await;
match result {
Ok(_) => {}
Err(ProbeError::MpvUnavailable(_)) => {}
Err(ProbeError::Timeout { .. }) => {}
Err(ProbeError::MpvCommandFailed { .. }) => {}
Err(ProbeError::MpvInitFailed(_)) => {}
Err(ProbeError::NoStreams(_)) => {}
Err(other) => panic!("unexpected error variant: {other:?}"),
}
}
}