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', b'\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: {}",
91 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(parse_time_str)
222}
223
224pub fn try_parse_output(mut string: &str) -> Option<FfmpegOutput> {
241 let raw_log_message = string.to_string();
242
243 string = string
244 .strip_prefix("[info]")
245 .unwrap_or(string)
246 .trim()
247 .strip_prefix("Output #")?;
248
249 let index = string
250 .split_whitespace()
251 .next()
252 .and_then(|s| s.split(',').next())
253 .and_then(|s| s.parse::<u32>().ok())?;
254
255 let to = string
256 .split(" to '")
257 .nth(1)?
258 .split('\'')
259 .next()?
260 .to_string();
261
262 Some(FfmpegOutput {
263 index,
264 to,
265 raw_log_message,
266 })
267}
268
269pub fn try_parse_stream(string: &str) -> Option<Stream> {
410 let raw_log_message = string.to_string();
411
412 let string = string
413 .strip_prefix("[info]")
414 .unwrap_or(string)
415 .trim()
416 .strip_prefix("Stream #")?;
417 let mut comma_iter = CommaIter::new(string);
418 let mut colon_iter = comma_iter.next()?.split(':');
419
420 let parent_index = colon_iter.next()?.parse::<u32>().ok()?;
421
422 let indices_and_maybe_language = colon_iter
424 .next()?
425 .split(|c| c == '[' || c == ']')
427 .step_by(2)
428 .collect::<String>();
429 let mut parenthesis_iter = indices_and_maybe_language.split('(');
430 let stream_index = parenthesis_iter.next()?.trim().parse::<u32>().ok()?;
431 let language = parenthesis_iter.next().map_or("".to_string(), |lang| {
432 lang.trim_end_matches(')').to_string()
433 });
434
435 let stream_type = colon_iter.next()?.trim();
437 let format = colon_iter
438 .next()?
439 .trim()
440 .split(&[' ', '(']) .next()?
442 .to_string();
443
444 let type_specific_data: StreamTypeSpecificData = match stream_type {
446 "Audio" => try_parse_audio_stream(comma_iter)?,
447 "Subtitle" => StreamTypeSpecificData::Subtitle(),
448 "Video" => try_parse_video_stream(comma_iter)?,
449 _ => StreamTypeSpecificData::Other(),
450 };
451
452 Some(Stream {
453 format,
454 language,
455 parent_index,
456 stream_index,
457 raw_log_message,
458 type_specific_data,
459 })
460}
461
462fn try_parse_audio_stream(mut comma_iter: CommaIter) -> Option<StreamTypeSpecificData> {
464 let sample_rate = comma_iter
465 .next()?
466 .split_whitespace()
467 .next()?
468 .parse::<u32>()
469 .ok()?;
470
471 let channels = comma_iter.next()?.trim().to_string();
472
473 Some(StreamTypeSpecificData::Audio(AudioStream {
474 sample_rate,
475 channels,
476 }))
477}
478
479fn try_parse_video_stream(mut comma_iter: CommaIter) -> Option<StreamTypeSpecificData> {
481 let pix_fmt = comma_iter
482 .next()?
483 .trim()
484 .split(&[' ', '(']) .next()?
486 .to_string();
487
488 let dims = comma_iter.next()?.split_whitespace().next()?;
489 let mut dims_iter = dims.split('x');
490 let width = dims_iter.next()?.parse::<u32>().ok()?;
491 let height = dims_iter.next()?.parse::<u32>().ok()?;
492
493 let fps = comma_iter
496 .find_map(|part| {
497 if part.trim().ends_with("fps") {
498 part.split_whitespace().next()
499 } else {
500 None
501 }
502 })
503 .and_then(|fps_str| fps_str.parse::<f32>().ok())?;
504
505 Some(StreamTypeSpecificData::Video(VideoStream {
506 pix_fmt,
507 width,
508 height,
509 fps,
510 }))
511}
512
513pub 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 .split_whitespace()
537 .next()?
538 .parse::<u32>()
539 .ok()?;
540 let fps = string
541 .split("fps=")
542 .nth(1)?
543 .split_whitespace()
544 .next()?
545 .parse::<f32>()
546 .ok()?;
547 let q = string
548 .split("q=")
549 .nth(1)?
550 .split_whitespace()
551 .next()?
552 .parse::<f32>()
553 .ok()?;
554 let size_kb = string
555 .split("size=") .nth(1)?
557 .split_whitespace()
558 .next()
559 .map(|s| s.trim())
560 .and_then(|s| {
561 s.strip_suffix("KiB") .or_else(|| s.strip_suffix("kB")) .or_else(|| s.ends_with("N/A").then(|| "0")) })?
565 .parse::<u32>()
566 .ok()?;
567 let time = string
568 .split("time=")
569 .nth(1)?
570 .split_whitespace()
571 .next()?
572 .to_string();
573 let bitrate_kbps = string
574 .split("bitrate=")
575 .nth(1)?
576 .split_whitespace()
577 .next()?
578 .trim()
579 .replace("kbits/s", "")
580 .parse::<f32>()
581 .unwrap_or(0.0); let speed = string
583 .split("speed=")
584 .nth(1)?
585 .split_whitespace()
586 .next()?
587 .strip_suffix('x')
588 .map(|s| s.parse::<f32>().unwrap_or(0.0))
589 .unwrap_or(0.0);
590
591 Some(FfmpegProgress {
592 frame,
593 fps,
594 q,
595 size_kb,
596 time,
597 bitrate_kbps,
598 speed,
599 raw_log_message,
600 })
601}
602
603pub fn parse_time_str(str: &str) -> Option<f64> {
620 let mut seconds = 0.0;
621
622 let mut smh = str.split(':').rev();
623 if let Some(sec) = smh.next() {
624 seconds += sec.parse::<f64>().ok()?;
625 }
626
627 if let Some(min) = smh.next() {
628 seconds += min.parse::<f64>().ok()? * 60.0;
629 }
630
631 if let Some(hrs) = smh.next() {
632 seconds += hrs.parse::<f64>().ok()? * 60.0 * 60.0;
633 }
634
635 Some(seconds)
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641 use crate::{command::BackgroundCommand, paths::ffmpeg_path};
642 use std::{
643 io::{Cursor, Seek, SeekFrom, Write},
644 process::{Command, Stdio},
645 };
646
647 #[test]
648 fn test_parse_version() {
649 let cmd = Command::new(ffmpeg_path())
650 .create_no_window()
651 .arg("-version")
652 .stdout(Stdio::piped())
653 .spawn()
655 .unwrap();
656
657 let stdout = cmd.stdout.unwrap();
658 let mut parser = FfmpegLogParser::new(stdout);
659 while let Ok(event) = parser.parse_next_event() {
660 if let FfmpegEvent::ParsedVersion(_) = event {
661 return;
662 }
663 }
664 panic!() }
666
667 #[test]
668 fn test_parse_configuration() {
669 let cmd = Command::new(ffmpeg_path())
670 .create_no_window()
671 .arg("-version")
672 .stdout(Stdio::piped())
673 .spawn()
675 .unwrap();
676
677 let stdout = cmd.stdout.unwrap();
678 let mut parser = FfmpegLogParser::new(stdout);
679 while let Ok(event) = parser.parse_next_event() {
680 if let FfmpegEvent::ParsedConfiguration(_) = event {
681 return;
682 }
683 }
684 panic!() }
686
687 #[test]
689 fn test_macos_line_endings() {
690 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";
691
692 let mut cursor = Cursor::new(Vec::new());
694 cursor.write_all(stdout_str.as_bytes()).unwrap();
695 cursor.seek(SeekFrom::Start(0)).unwrap();
696
697 let reader = BufReader::new(cursor);
698 let mut parser = FfmpegLogParser::new(reader);
699 let mut num_events = 0;
700 while let Ok(event) = parser.parse_next_event() {
701 match event {
702 FfmpegEvent::LogEOF => break,
703 _ => num_events += 1,
704 }
705 }
706 assert!(num_events > 1);
707 }
708
709 #[test]
713 fn test_parse_progress_v7() {
714 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";
715 let progress = try_parse_progress(line).unwrap();
716 assert!(progress.frame == 5);
717 assert!(progress.fps == 0.0);
718 assert!(progress.q == -1.0);
719 assert!(progress.size_kb == 10);
720 assert!(progress.time == "00:00:03.00");
721 assert!(progress.bitrate_kbps == 27.2);
722 assert!(progress.speed == 283.0);
723 }
724
725 #[test]
728 fn test_parse_progress_empty() {
729 let line =
730 "[info] frame= 0 fps=0.0 q=-0.0 size= 0kB time=00:00:00.00 bitrate=N/A speed=N/A\n";
731 let progress = try_parse_progress(line).unwrap();
732 assert!(progress.frame == 0);
733 assert!(progress.fps == 0.0);
734 assert!(progress.q == -0.0);
735 assert!(progress.size_kb == 0);
736 assert!(progress.time == "00:00:00.00");
737 assert!(progress.bitrate_kbps == 0.0);
738 assert!(progress.speed == 0.0);
739 }
740
741 #[test]
744 fn test_parse_progress_no_size() {
745 let line = "[info] frame= 163 fps= 13 q=4.4 size=N/A time=00:13:35.00 bitrate=N/A speed=64.7x";
746 let progress = try_parse_progress(line).unwrap();
747 assert!(progress.frame == 163);
748 assert!(progress.fps == 13.0);
749 assert!(progress.q == 4.4);
750 assert!(progress.size_kb == 0);
751 assert!(progress.time == "00:13:35.00");
752 assert!(progress.bitrate_kbps == 0.0);
753 assert!(progress.speed == 64.7);
754 }
755
756 #[test]
758 fn test_non_utf8() -> anyhow::Result<()> {
759 let mut cursor = Cursor::new(Vec::new());
761 cursor
762 .write_all(b"[info] \x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\n")
763 .unwrap();
764 cursor.seek(SeekFrom::Start(0)).unwrap();
765
766 let event = FfmpegLogParser::new(cursor).parse_next_event()?;
767
768 assert!(matches!(event, FfmpegEvent::Log(LogLevel::Info, _)));
769
770 Ok(())
771 }
772}