diff --git a/src/player/ffmpeg.rs b/src/player/ffmpeg.rs new file mode 100644 index 0000000..15ef7f4 --- /dev/null +++ b/src/player/ffmpeg.rs @@ -0,0 +1,155 @@ +//! FFmpeg 封裝 + +use anyhow::{Context, Result}; +use std::path::Path; +use std::process::{Command, Stdio, Child, ChildStdout}; +use std::io::{Read, BufReader}; +use std::sync::{Arc, Mutex}; + +#[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, + stdout: Option>, + info: VideoInfo, +} + +impl FFmpegDecoder { + pub fn new(path: &Path) -> Result { + 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 { + 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::().unwrap_or(30.0), parts[1].parse::().unwrap_or(1.0)) + } else { + (fps_str.parse::().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>> { + 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(); + } +}