Files
momentry_playground/src/player/ffmpeg.rs

164 lines
4.4 KiB
Rust

//! FFmpeg wrapper
use anyhow::{Context, Result};
use std::io::{BufReader, Read};
use std::path::Path;
use std::process::{Child, ChildStdout, Command, Stdio};
#[derive(Debug, Clone)]
pub struct VideoInfo {
pub width: u32,
pub height: u32,
pub fps: f64,
pub duration_ms: u64,
pub frame_count: u64,
pub codec: String,
}
pub struct FFmpegDecoder {
path: String,
process: Option<Child>,
stdout: Option<BufReader<ChildStdout>>,
info: VideoInfo,
}
impl FFmpegDecoder {
pub fn new(path: &Path) -> Result<Self> {
let path_str = path.to_string_lossy().to_string();
let info = Self::probe(path)?;
Ok(Self {
path: path_str,
process: None,
stdout: None,
info,
})
}
fn probe(path: &Path) -> Result<VideoInfo> {
let output = Command::new("ffprobe")
.args([
"-v",
"quiet",
"-print_format",
"json",
"-show_format",
"-show_streams",
path.to_str().unwrap_or(""),
])
.output()
.context("Failed to run ffprobe")?;
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).context("Failed to parse ffprobe output")?;
let video_stream = json["streams"]
.as_array()
.and_then(|streams| streams.iter().find(|s| s["codec_type"] == "video"))
.context("No video stream found")?;
let width = video_stream["width"].as_u64().unwrap_or(0) as u32;
let height = video_stream["height"].as_u64().unwrap_or(0) as u32;
let fps_str = video_stream["r_frame_rate"].as_str().unwrap_or("30/1");
let (num, den) = {
let parts: Vec<&str> = fps_str.split('/').collect();
if parts.len() == 2 {
(
parts[0].parse::<f64>().unwrap_or(30.0),
parts[1].parse::<f64>().unwrap_or(1.0),
)
} else {
(fps_str.parse::<f64>().unwrap_or(30.0), 1.0)
}
};
let fps = num / den;
let duration_str = json["format"]["duration"].as_str().unwrap_or("0");
let duration_sec: f64 = duration_str.parse().unwrap_or(0.0);
let duration_ms = (duration_sec * 1000.0) as u64;
let frame_count = (duration_sec * fps) as u64;
let codec = video_stream["codec_name"]
.as_str()
.unwrap_or("unknown")
.to_string();
Ok(VideoInfo {
width,
height,
fps,
duration_ms,
frame_count,
codec,
})
}
pub fn get_info(&self) -> VideoInfo {
self.info.clone()
}
pub fn start_decoding(&mut self, start_ms: u64) -> Result<()> {
self.stop();
let start_sec = start_ms as f64 / 1000.0;
let mut child = Command::new("ffmpeg")
.args([
"-ss",
&format!("{}", start_sec),
"-i",
&self.path,
"-f",
"rawvideo",
"-pix_fmt",
"rgb24",
"-",
])
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.context("Failed to start ffmpeg")?;
let stdout = child.stdout.take().context("Failed to capture stdout")?;
self.process = Some(child);
self.stdout = Some(BufReader::new(stdout));
Ok(())
}
pub fn seek(&mut self, timestamp_ms: u64) -> Result<()> {
self.start_decoding(timestamp_ms)
}
pub fn stop(&mut self) {
if let Some(mut child) = self.process.take() {
let _ = child.kill();
let _ = child.wait();
}
self.stdout = None;
}
pub fn read_frame(&mut self) -> Result<Option<Vec<u8>>> {
let frame_size = (self.info.width * self.info.height * 3) as usize;
let mut buffer = vec![0u8; frame_size];
if let Some(ref mut reader) = self.stdout {
match reader.read_exact(&mut buffer) {
Ok(_) => Ok(Some(buffer)),
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => Ok(None),
Err(e) => Err(e.into()),
}
} else {
Ok(None)
}
}
}
impl Drop for FFmpegDecoder {
fn drop(&mut self) {
self.stop();
}
}