use std::{
io::{BufReader, Read},
str::from_utf8,
};
use crate::{
comma_iter::CommaIter,
event::{
AVStream, FfmpegConfiguration, FfmpegDuration, FfmpegEvent, FfmpegInput, FfmpegOutput,
FfmpegProgress, FfmpegVersion, LogLevel,
},
read_until_any::read_until_any,
};
#[derive(Debug, Clone, PartialEq)]
enum LogSection {
Input(u32),
Output(u32),
StreamMapping,
Other,
}
pub struct FfmpegLogParser<R: Read> {
reader: BufReader<R>,
cur_section: LogSection,
}
impl<R: Read> FfmpegLogParser<R> {
pub fn parse_next_event(&mut self) -> anyhow::Result<FfmpegEvent> {
let mut buf = Vec::<u8>::new();
let bytes_read = read_until_any(&mut self.reader, &[b'\r', b'\n'], &mut buf);
let line = from_utf8(buf.as_slice())?.trim();
let raw_log_message = line.to_string();
match bytes_read? {
0 => Ok(FfmpegEvent::LogEOF),
_ => {
if let Some(input_number) = try_parse_input(line) {
self.cur_section = LogSection::Input(input_number);
return Ok(FfmpegEvent::ParsedInput(FfmpegInput {
index: input_number,
duration: None,
raw_log_message,
}));
} else if let Some(output) = try_parse_output(line) {
self.cur_section = LogSection::Output(output.index);
return Ok(FfmpegEvent::ParsedOutput(output));
} else if line.contains("Stream mapping:") {
self.cur_section = LogSection::StreamMapping;
}
if let Some(version) = try_parse_version(line) {
Ok(FfmpegEvent::ParsedVersion(FfmpegVersion {
version,
raw_log_message,
}))
} else if let Some(configuration) = try_parse_configuration(line) {
Ok(FfmpegEvent::ParsedConfiguration(FfmpegConfiguration {
configuration,
raw_log_message,
}))
} else if let Some(duration) = try_parse_duration(line) {
match self.cur_section {
LogSection::Input(input_index) => Ok(FfmpegEvent::ParsedDuration(FfmpegDuration {
input_index,
duration,
raw_log_message,
})),
_ => Ok(FfmpegEvent::Log(LogLevel::Info, line.to_string())),
}
} else if self.cur_section == LogSection::StreamMapping && line.contains(" Stream #") {
Ok(FfmpegEvent::ParsedStreamMapping(line.to_string()))
} else if let Some(stream) = try_parse_stream(line) {
match self.cur_section {
LogSection::Input(_) => Ok(FfmpegEvent::ParsedInputStream(stream)),
LogSection::Output(_) => Ok(FfmpegEvent::ParsedOutputStream(stream)),
LogSection::Other | LogSection::StreamMapping => Err(anyhow::Error::msg(format!(
"Unexpected stream specification: {}",
line
))),
}
} else if let Some(progress) = try_parse_progress(line) {
self.cur_section = LogSection::Other;
Ok(FfmpegEvent::Progress(progress))
} else if line.contains("[info]") {
Ok(FfmpegEvent::Log(LogLevel::Info, line.to_string()))
} else if line.contains("[warning]") {
Ok(FfmpegEvent::Log(LogLevel::Warning, line.to_string()))
} else if line.contains("[error]") {
Ok(FfmpegEvent::Log(LogLevel::Error, line.to_string()))
} else if line.contains("[fatal]") {
Ok(FfmpegEvent::Log(LogLevel::Fatal, line.to_string()))
} else {
Ok(FfmpegEvent::Log(LogLevel::Unknown, line.to_string()))
}
}
}
}
pub fn new(inner: R) -> Self {
Self {
reader: BufReader::new(inner),
cur_section: LogSection::Other,
}
}
}
pub fn try_parse_version(string: &str) -> Option<String> {
string
.strip_prefix("[info]")
.unwrap_or(string)
.trim()
.strip_prefix("ffmpeg version ")?
.split_whitespace()
.next()
.map(|s| s.to_string())
}
pub fn try_parse_configuration(string: &str) -> Option<Vec<String>> {
string
.strip_prefix("[info]")
.unwrap_or(string)
.trim()
.strip_prefix("configuration: ")
.map(|s| s.split_whitespace().map(|s| s.to_string()).collect())
}
pub fn try_parse_input(string: &str) -> Option<u32> {
string
.strip_prefix("[info]")
.unwrap_or(string)
.trim()
.strip_prefix("Input #")?
.split_whitespace()
.next()
.and_then(|s| s.split(',').next())
.and_then(|s| s.parse::<u32>().ok())
}
pub fn try_parse_duration(string: &str) -> Option<f64> {
string
.strip_prefix("[info]")
.unwrap_or(string)
.trim()
.strip_prefix("Duration:")?
.trim()
.split(',')
.next()
.and_then(parse_time_str)
}
pub fn try_parse_output(mut string: &str) -> Option<FfmpegOutput> {
let raw_log_message = string.to_string();
string = string
.strip_prefix("[info]")
.unwrap_or(string)
.trim()
.strip_prefix("Output #")?;
let index = string
.split_whitespace()
.next()
.and_then(|s| s.split(',').next())
.and_then(|s| s.parse::<u32>().ok())?;
let to = string
.split(" to '")
.nth(1)?
.split('\'')
.next()?
.to_string();
Some(FfmpegOutput {
index,
to,
raw_log_message,
})
}
pub fn try_parse_stream(mut string: &str) -> Option<AVStream> {
let raw_log_message = string.to_string();
string = string
.strip_prefix("[info]")
.unwrap_or(string)
.trim()
.strip_prefix("Stream #")?;
let mut colon_parts = string.split(':');
let parent_index = colon_parts.next()?.parse::<usize>().ok()?;
let stream_type = colon_parts.nth(1)?.trim().to_string();
if stream_type != "Video" {
return Some(AVStream {
stream_type,
format: "unknown".into(),
pix_fmt: "unknown".into(),
width: 0,
height: 0,
fps: 0.0,
parent_index,
raw_log_message,
});
}
let comma_string = colon_parts.next()?.trim();
let mut comma_iter = CommaIter::new(comma_string);
let format = comma_iter
.next()?
.trim()
.split(&[' ', '(']) .next()?
.to_string();
let pix_fmt = comma_iter
.next()?
.trim()
.split(&[' ', '(']) .next()?
.to_string();
let dims = comma_iter.next()?.split_whitespace().next()?;
let mut dims_iter = dims.split('x');
let width = dims_iter.next()?.parse::<u32>().ok()?;
let height = dims_iter.next()?.parse::<u32>().ok()?;
let fps = string
.split("fps,")
.next()?
.split_whitespace()
.last()?
.parse()
.ok()?;
Some(AVStream {
stream_type,
parent_index,
format,
pix_fmt,
width,
height,
fps,
raw_log_message,
})
}
pub fn try_parse_progress(mut string: &str) -> Option<FfmpegProgress> {
let raw_log_message = string.to_string();
string = string.strip_prefix("[info]").unwrap_or(string).trim();
let frame = string
.split("frame=")
.nth(1)?
.split_whitespace()
.next()?
.parse::<u32>()
.ok()?;
let fps = string
.split("fps=")
.nth(1)?
.split_whitespace()
.next()?
.parse::<f32>()
.ok()?;
let q = string
.split("q=")
.nth(1)?
.split_whitespace()
.next()?
.parse::<f32>()
.ok()?;
let size_kb = string
.split("size=") .nth(1)?
.split_whitespace()
.next()?
.trim()
.strip_suffix("kB")?
.parse::<u32>()
.ok()?;
let time = string
.split("time=")
.nth(1)?
.split_whitespace()
.next()?
.to_string();
let bitrate_kbps = string
.split("bitrate=")
.nth(1)?
.split_whitespace()
.next()?
.trim()
.strip_suffix("kbits/s")?
.parse::<f32>()
.ok()?;
let speed = string
.split("speed=")
.nth(1)?
.split_whitespace()
.next()?
.strip_suffix('x')
.map(|s| s.parse::<f32>().unwrap_or(0.0))
.unwrap_or(0.0);
Some(FfmpegProgress {
frame,
fps,
q,
size_kb,
time,
bitrate_kbps,
speed,
raw_log_message,
})
}
pub fn parse_time_str(str: &str) -> Option<f64> {
let mut seconds = 0.0;
let mut smh = str.split(':').rev();
if let Some(sec) = smh.next() {
seconds += sec.parse::<f64>().ok()?;
}
if let Some(min) = smh.next() {
seconds += min.parse::<f64>().ok()? * 60.0;
}
if let Some(hrs) = smh.next() {
seconds += hrs.parse::<f64>().ok()? * 60.0 * 60.0;
}
Some(seconds)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::paths::ffmpeg_path;
use std::{
io::{Cursor, Seek, SeekFrom, Write},
process::{Command, Stdio},
};
#[test]
fn test_parse_version() {
let cmd = Command::new(ffmpeg_path())
.arg("-version")
.stdout(Stdio::piped())
.spawn()
.unwrap();
let stdout = cmd.stdout.unwrap();
let mut parser = FfmpegLogParser::new(stdout);
while let Ok(event) = parser.parse_next_event() {
if let FfmpegEvent::ParsedVersion(_) = event {
return;
}
}
panic!() }
#[test]
fn test_parse_configuration() {
let cmd = Command::new(ffmpeg_path())
.arg("-version")
.stdout(Stdio::piped())
.spawn()
.unwrap();
let stdout = cmd.stdout.unwrap();
let mut parser = FfmpegLogParser::new(stdout);
while let Ok(event) = parser.parse_next_event() {
if let FfmpegEvent::ParsedConfiguration(_) = event {
return;
}
}
panic!() }
#[test]
fn test_macos_line_endings() {
let stdout_str = "[info] ffmpeg version N-109875-geabc304d12-tessus https://evermeet.cx/ffmpeg/ Copyright (c) 2000-2023 the FFmpeg developers\n[info] built with Apple clang version 11.0.0 (clang-1100.0.33.17)\n[info] configuration: --cc=/usr/bin/clang --prefix=/opt/ffmpeg --extra-version=tessus --enable-avisynth --enable-fontconfig --enable-gpl --enable-libaom --enable-libass --enable-libbluray --enable-libdav1d --enable-libfreetype --enable-libgsm --enable-libmodplug --enable-libmp3lame --enable-libmysofa --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenh264 --enable-libopenjpeg --enable-libopus --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvmaf --enable-libvo-amrwbenc --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxavs --enable-libxvid --enable-libzimg --enable-libzmq --enable-libzvbi --enable-version3 --pkg-config-flags=--static --disable-ffplay\n[info] libavutil 58. 1.100 / 58. 1.100\n[info] libavcodec 60. 2.100 / 60. 2.100\n[info] libavformat 60. 2.100 / 60. 2.100\n[info] libavdevice 60. 0.100 / 60. 0.100\n[info] libavfilter 9. 2.100 / 9. 2.100\n[info] libswscale 7. 0.100 / 7. 0.100\n[info] libswresample 4. 9.100 / 4. 9.100\n[info] libpostproc 57. 0.100 / 57. 0.100\n[info] Input #0, lavfi, from 'testsrc=duration=10':\n[info] Duration: N/A, start: 0.000000, bitrate: N/A\n[info] Stream #0:0: Video: wrapped_avframe, rgb24, 320x240 [SAR 1:1 DAR 4:3], 25 fps, 25 tbr, 25 tbn\n[info] Stream mapping:\n[info] Stream #0:0 -> #0:0 (wrapped_avframe (native) -> rawvideo (native))\n[info] Press [q] to stop, [?] for help\n[info] Output #0, rawvideo, to 'pipe:':\n[info] Metadata:\n[info] encoder : Lavf60.2.100\n[info] Stream #0:0: Video: rawvideo (RGB[24] / 0x18424752), rgb24(progressive), 320x240 [SAR 1:1 DAR 4:3], q=2-31, 46080 kb/s, 25 fps, 25 tbn\n[info] Metadata:\n[info] encoder : Lavc60.2.100 rawvideo\n[info] frame= 0 fps=0.0 q=0.0 size= 0kB time=-577014:32:22.77 bitrate= -0.0kbits/s speed=N/A";
let mut cursor = Cursor::new(Vec::new());
cursor.write_all(stdout_str.as_bytes()).unwrap();
cursor.seek(SeekFrom::Start(0)).unwrap();
let reader = BufReader::new(cursor);
let mut parser = FfmpegLogParser::new(reader);
let mut num_events = 0;
while let Ok(event) = parser.parse_next_event() {
match event {
FfmpegEvent::LogEOF => break,
_ => num_events += 1,
}
}
assert!(num_events > 1);
}
}