1use std::io::{BufReader, Read};
4
5use crate::{
6 comma_iter::CommaIter,
7 event::{
8 AudioStream, FfmpegConfiguration, FfmpegDuration, FfmpegEvent, FfmpegInput, FfmpegOutput,
9 FfmpegProgress, FfmpegVersion, LogLevel, Stream, StreamTypeSpecificData, VideoStream,
10 },
11 read_until_any::read_until_any,
12};
13
14#[derive(Debug, Clone, PartialEq)]
15enum LogSection {
16 Input(u32),
17 Output(u32),
18 StreamMapping,
19 Other,
20}
21
22pub struct FfmpegLogParser<R: Read> {
23 reader: BufReader<R>,
24 cur_section: LogSection,
25}
26
27impl<R: Read> FfmpegLogParser<R> {
28 pub fn parse_next_event(&mut self) -> anyhow::Result<FfmpegEvent> {
40 let mut buf = Vec::<u8>::new();
41 let bytes_read = read_until_any(&mut self.reader, b"\r\n", &mut buf);
42 let line_cow = String::from_utf8_lossy(buf.as_slice());
43 let line = line_cow.trim();
44 let raw_log_message = line.to_string();
45 match bytes_read? {
46 0 => Ok(FfmpegEvent::LogEOF),
47 _ => {
48 if let Some(input_number) = try_parse_input(line) {
50 self.cur_section = LogSection::Input(input_number);
51 return Ok(FfmpegEvent::ParsedInput(FfmpegInput {
52 index: input_number,
53 duration: None,
54 raw_log_message,
55 }));
56 } else if let Some(output) = try_parse_output(line) {
57 self.cur_section = LogSection::Output(output.index);
58 return Ok(FfmpegEvent::ParsedOutput(output));
59 } else if line.contains("Stream mapping:") {
60 self.cur_section = LogSection::StreamMapping;
61 }
62
63 if let Some(version) = try_parse_version(line) {
65 Ok(FfmpegEvent::ParsedVersion(FfmpegVersion {
66 version,
67 raw_log_message,
68 }))
69 } else if let Some(configuration) = try_parse_configuration(line) {
70 Ok(FfmpegEvent::ParsedConfiguration(FfmpegConfiguration {
71 configuration,
72 raw_log_message,
73 }))
74 } else if let Some(duration) = try_parse_duration(line) {
75 match self.cur_section {
76 LogSection::Input(input_index) => Ok(FfmpegEvent::ParsedDuration(FfmpegDuration {
77 input_index,
78 duration,
79 raw_log_message,
80 })),
81 _ => Ok(FfmpegEvent::Log(LogLevel::Info, line.to_string())),
82 }
83 } else if self.cur_section == LogSection::StreamMapping && line.contains(" Stream #") {
84 Ok(FfmpegEvent::ParsedStreamMapping(line.to_string()))
85 } else if let Some(stream) = try_parse_stream(line) {
86 match self.cur_section {
87 LogSection::Input(_) => Ok(FfmpegEvent::ParsedInputStream(stream)),
88 LogSection::Output(_) => Ok(FfmpegEvent::ParsedOutputStream(stream)),
89 LogSection::Other | LogSection::StreamMapping => Err(anyhow::Error::msg(format!(
90 "Unexpected stream specification: {line}"
91 ))),
92 }
93 } else if let Some(progress) = try_parse_progress(line) {
94 self.cur_section = LogSection::Other;
95 Ok(FfmpegEvent::Progress(progress))
96 } else if line.contains("[info]") {
97 Ok(FfmpegEvent::Log(LogLevel::Info, line.to_string()))
98 } else if line.contains("[warning]") {
99 Ok(FfmpegEvent::Log(LogLevel::Warning, line.to_string()))
100 } else if line.contains("[error]") {
101 Ok(FfmpegEvent::Log(LogLevel::Error, line.to_string()))
102 } else if line.contains("[fatal]") {
103 Ok(FfmpegEvent::Log(LogLevel::Fatal, line.to_string()))
104 } else {
105 Ok(FfmpegEvent::Log(LogLevel::Unknown, line.to_string()))
106 }
107 }
108 }
109 }
110
111 pub fn new(inner: R) -> Self {
112 Self {
113 reader: BufReader::new(inner),
114 cur_section: LogSection::Other,
115 }
116 }
117}
118
119pub fn try_parse_version(string: &str) -> Option<String> {
132 string
133 .strip_prefix("[info]")
134 .unwrap_or(string)
135 .trim()
136 .strip_prefix("ffmpeg version ")?
137 .split_whitespace()
138 .next()
139 .map(|s| s.to_string())
140}
141
142pub fn try_parse_configuration(string: &str) -> Option<Vec<String>> {
162 string
163 .strip_prefix("[info]")
164 .unwrap_or(string)
165 .trim()
166 .strip_prefix("configuration: ")
167 .map(|s| s.split_whitespace().map(|s| s.to_string()).collect())
168}
169
170pub fn try_parse_input(string: &str) -> Option<u32> {
182 string
183 .strip_prefix("[info]")
184 .unwrap_or(string)
185 .trim()
186 .strip_prefix("Input #")?
187 .split_whitespace()
188 .next()
189 .and_then(|s| s.split(',').next())
190 .and_then(|s| s.parse::<u32>().ok())
191}
192
193pub fn try_parse_duration(string: &str) -> Option<f64> {
212 string
213 .strip_prefix("[info]")
214 .unwrap_or(string)
215 .trim()
216 .strip_prefix("Duration:")?
217 .trim()
218 .split(',')
219 .next()
220 .and_then(parse_time_str)
221}
222
223pub fn try_parse_output(mut string: &str) -> Option<FfmpegOutput> {
240 let raw_log_message = string.to_string();
241
242 string = string
243 .strip_prefix("[info]")
244 .unwrap_or(string)
245 .trim()
246 .strip_prefix("Output #")?;
247
248 let index = string
249 .split_whitespace()
250 .next()
251 .and_then(|s| s.split(',').next())
252 .and_then(|s| s.parse::<u32>().ok())?;
253
254 let to = string
255 .split(" to '")
256 .nth(1)?
257 .split('\'')
258 .next()?
259 .to_string();
260
261 Some(FfmpegOutput {
262 index,
263 to,
264 raw_log_message,
265 })
266}
267
268pub fn try_parse_stream(string: &str) -> Option<Stream> {
409 let raw_log_message = string.to_string();
410
411 let string = string
412 .strip_prefix("[info]")
413 .unwrap_or(string)
414 .trim()
415 .strip_prefix("Stream #")?;
416 let mut comma_iter = CommaIter::new(string);
417 let mut colon_iter = comma_iter.next()?.split(':');
418
419 let parent_index = colon_iter.next()?.parse::<u32>().ok()?;
420
421 let indices_and_maybe_language = colon_iter
423 .next()?
424 .split(['[', ']'])
426 .step_by(2)
427 .collect::<String>();
428 let mut parenthesis_iter = indices_and_maybe_language.split('(');
429 let stream_index = parenthesis_iter.next()?.trim().parse::<u32>().ok()?;
430 let language = parenthesis_iter.next().map_or("".to_string(), |lang| {
431 lang.trim_end_matches(')').to_string()
432 });
433
434 let stream_type = colon_iter.next()?.trim();
436 let format = colon_iter
437 .next()?
438 .trim()
439 .split(&[' ', '(']) .next()?
441 .to_string();
442
443 let type_specific_data: StreamTypeSpecificData = match stream_type {
445 "Audio" => try_parse_audio_stream(comma_iter)?,
446 "Subtitle" => StreamTypeSpecificData::Subtitle(),
447 "Video" => try_parse_video_stream(comma_iter)?,
448 _ => StreamTypeSpecificData::Other(),
449 };
450
451 Some(Stream {
452 format,
453 language,
454 parent_index,
455 stream_index,
456 raw_log_message,
457 type_specific_data,
458 })
459}
460
461fn try_parse_audio_stream(mut comma_iter: CommaIter) -> Option<StreamTypeSpecificData> {
463 let sample_rate = comma_iter
464 .next()?
465 .split_whitespace()
466 .next()?
467 .parse::<u32>()
468 .ok()?;
469
470 let channels = comma_iter.next()?.trim().to_string();
471
472 Some(StreamTypeSpecificData::Audio(AudioStream {
473 sample_rate,
474 channels,
475 }))
476}
477
478fn try_parse_video_stream(mut comma_iter: CommaIter) -> Option<StreamTypeSpecificData> {
480 let pix_fmt = comma_iter
481 .next()?
482 .trim()
483 .split(&[' ', '(']) .next()?
485 .to_string();
486
487 let dims = comma_iter.next()?.split_whitespace().next()?;
488 let mut dims_iter = dims.split('x');
489 let width = dims_iter.next()?.parse::<u32>().ok()?;
490 let height = dims_iter.next()?.parse::<u32>().ok()?;
491
492 let fps = comma_iter
495 .find_map(|part| {
496 if part.trim().ends_with("fps") {
497 part.split_whitespace().next()
498 } else {
499 None
500 }
501 })
502 .and_then(|fps_str| fps_str.parse::<f32>().ok())?;
503
504 Some(StreamTypeSpecificData::Video(VideoStream {
505 pix_fmt,
506 width,
507 height,
508 fps,
509 }))
510}
511
512pub fn try_parse_progress(mut string: &str) -> Option<FfmpegProgress> {
529 let raw_log_message = string.to_string();
530
531 string = string.strip_prefix("[info]").unwrap_or(string).trim();
532
533 let frame = string
534 .split("frame=")
535 .nth(1)
536 .and_then(|s| s.split_whitespace().next())
537 .and_then(|s| s.parse::<u32>().ok())
538 .unwrap_or(0);
539 let fps = string
540 .split("fps=")
541 .nth(1)
542 .and_then(|s| s.split_whitespace().next())
543 .and_then(|s| s.parse::<f32>().ok())
544 .unwrap_or(0.0);
545 let q = string
546 .split("q=")
547 .nth(1)
548 .and_then(|s| s.split_whitespace().next())
549 .and_then(|s| s.parse::<f32>().ok())
550 .unwrap_or(0.0);
551 let size_kb = string
552 .split("size=") .nth(1)?
554 .split_whitespace()
555 .next()
556 .map(|s| s.trim())
557 .and_then(|s| {
558 s.strip_suffix("KiB") .or_else(|| s.strip_suffix("kB")) .or_else(|| s.ends_with("N/A").then_some("0")) })?
562 .parse::<u32>()
563 .ok()?;
564 let time = string
565 .split("time=")
566 .nth(1)?
567 .split_whitespace()
568 .next()?
569 .to_string();
570 let bitrate_kbps = string
571 .split("bitrate=")
572 .nth(1)?
573 .split_whitespace()
574 .next()?
575 .trim()
576 .replace("kbits/s", "")
577 .parse::<f32>()
578 .unwrap_or(0.0); let speed = string
580 .split("speed=")
581 .nth(1)?
582 .split_whitespace()
583 .next()?
584 .strip_suffix('x')
585 .map(|s| s.parse::<f32>().unwrap_or(0.0))
586 .unwrap_or(0.0);
587
588 Some(FfmpegProgress {
589 frame,
590 fps,
591 q,
592 size_kb,
593 time,
594 bitrate_kbps,
595 speed,
596 raw_log_message,
597 })
598}
599
600pub fn parse_time_str(str: &str) -> Option<f64> {
617 let mut seconds = 0.0;
618
619 let mut smh = str.split(':').rev();
620 if let Some(sec) = smh.next() {
621 seconds += sec.parse::<f64>().ok()?;
622 }
623
624 if let Some(min) = smh.next() {
625 seconds += min.parse::<f64>().ok()? * 60.0;
626 }
627
628 if let Some(hrs) = smh.next() {
629 seconds += hrs.parse::<f64>().ok()? * 60.0 * 60.0;
630 }
631
632 Some(seconds)
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638 use crate::{command::BackgroundCommand, paths::ffmpeg_path};
639 use std::{
640 io::{Cursor, Seek, SeekFrom, Write},
641 process::{Command, Stdio},
642 };
643
644 #[test]
645 fn test_parse_version() {
646 let cmd = Command::new(ffmpeg_path())
647 .create_no_window()
648 .arg("-version")
649 .stdout(Stdio::piped())
650 .spawn()
652 .unwrap();
653
654 let stdout = cmd.stdout.unwrap();
655 let mut parser = FfmpegLogParser::new(stdout);
656 while let Ok(event) = parser.parse_next_event() {
657 if let FfmpegEvent::ParsedVersion(_) = event {
658 return;
659 }
660 }
661 panic!() }
663
664 #[test]
665 fn test_parse_configuration() {
666 let cmd = Command::new(ffmpeg_path())
667 .create_no_window()
668 .arg("-version")
669 .stdout(Stdio::piped())
670 .spawn()
672 .unwrap();
673
674 let stdout = cmd.stdout.unwrap();
675 let mut parser = FfmpegLogParser::new(stdout);
676 while let Ok(event) = parser.parse_next_event() {
677 if let FfmpegEvent::ParsedConfiguration(_) = event {
678 return;
679 }
680 }
681 panic!() }
683
684 #[test]
686 fn test_macos_line_endings() {
687 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";
688
689 let mut cursor = Cursor::new(Vec::new());
691 cursor.write_all(stdout_str.as_bytes()).unwrap();
692 cursor.seek(SeekFrom::Start(0)).unwrap();
693
694 let reader = BufReader::new(cursor);
695 let mut parser = FfmpegLogParser::new(reader);
696 let mut num_events = 0;
697 while let Ok(event) = parser.parse_next_event() {
698 match event {
699 FfmpegEvent::LogEOF => break,
700 _ => num_events += 1,
701 }
702 }
703 assert!(num_events > 1);
704 }
705
706 #[test]
710 fn test_parse_progress_v7() {
711 let line = "[info] frame= 5 fps=0.0 q=-1.0 Lsize= 10KiB time=00:00:03.00 bitrate= 27.2kbits/s speed= 283x\n";
712 let progress = try_parse_progress(line).unwrap();
713 assert_eq!(progress.frame, 5);
714 assert_eq!(progress.fps, 0.0);
715 assert_eq!(progress.q, -1.0);
716 assert_eq!(progress.size_kb, 10);
717 assert_eq!(progress.time, "00:00:03.00");
718 assert_eq!(progress.bitrate_kbps, 27.2);
719 assert_eq!(progress.speed, 283.0);
720 }
721
722 #[test]
725 fn test_parse_progress_empty() {
726 let line =
727 "[info] frame= 0 fps=0.0 q=-0.0 size= 0kB time=00:00:00.00 bitrate=N/A speed=N/A\n";
728 let progress = try_parse_progress(line).unwrap();
729 assert_eq!(progress.frame, 0);
730 assert_eq!(progress.fps, 0.0);
731 assert_eq!(progress.q, -0.0);
732 assert_eq!(progress.size_kb, 0);
733 assert_eq!(progress.time, "00:00:00.00");
734 assert_eq!(progress.bitrate_kbps, 0.0);
735 assert_eq!(progress.speed, 0.0);
736 }
737
738 #[test]
741 fn test_parse_progress_no_size() {
742 let line = "[info] frame= 163 fps= 13 q=4.4 size=N/A time=00:13:35.00 bitrate=N/A speed=64.7x";
743 let progress = try_parse_progress(line).unwrap();
744 assert_eq!(progress.frame, 163);
745 assert_eq!(progress.fps, 13.0);
746 assert_eq!(progress.q, 4.4);
747 assert_eq!(progress.size_kb, 0);
748 assert_eq!(progress.time, "00:13:35.00");
749 assert_eq!(progress.bitrate_kbps, 0.0);
750 assert_eq!(progress.speed, 64.7);
751 }
752
753 #[test]
755 fn test_non_utf8() -> anyhow::Result<()> {
756 let mut cursor = Cursor::new(Vec::new());
758 cursor
759 .write_all(b"[info] \x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\n")
760 .unwrap();
761 cursor.seek(SeekFrom::Start(0)).unwrap();
762
763 let event = FfmpegLogParser::new(cursor).parse_next_event()?;
764
765 assert!(matches!(event, FfmpegEvent::Log(LogLevel::Info, _)));
766
767 Ok(())
768 }
769
770 #[test]
771 fn test_audio_progress() {
772 let line = "[info] size= 66kB time=00:00:02.21 bitrate= 245.0kbits/s speed=1.07x\n";
773 let progress = try_parse_progress(line).unwrap();
774 assert!(progress.frame == 0);
775 assert!(progress.fps == 0.0);
776 assert!(progress.q == 0.0);
777 assert!(progress.size_kb == 66);
778 assert!(progress.time == "00:00:02.21");
779 assert!(progress.bitrate_kbps == 245.0);
780 assert!(progress.speed == 1.07);
781 }
782}