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