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")) })?
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 let mut seconds = 0.0;
620
621 let mut smh = str.split(':').rev();
622 if let Some(sec) = smh.next() {
623 seconds += sec.parse::<f64>().ok()?;
624 }
625
626 if let Some(min) = smh.next() {
627 seconds += min.parse::<f64>().ok()? * 60.0;
628 }
629
630 if let Some(hrs) = smh.next() {
631 seconds += hrs.parse::<f64>().ok()? * 60.0 * 60.0;
632 }
633
634 Some(seconds)
635}
636
637#[cfg(test)]
638mod tests {
639 use super::*;
640 use crate::{command::BackgroundCommand, paths::ffmpeg_path};
641 use std::{
642 io::{Cursor, Seek, SeekFrom, Write},
643 process::{Command, Stdio},
644 };
645
646 #[test]
647 fn test_parse_version() {
648 let cmd = Command::new(ffmpeg_path())
649 .create_no_window()
650 .arg("-version")
651 .stdout(Stdio::piped())
652 .spawn()
654 .unwrap();
655
656 let stdout = cmd.stdout.unwrap();
657 let mut parser = FfmpegLogParser::new(stdout);
658 while let Ok(event) = parser.parse_next_event() {
659 if let FfmpegEvent::ParsedVersion(_) = event {
660 return;
661 }
662 }
663 panic!() }
665
666 #[test]
667 fn test_parse_configuration() {
668 let cmd = Command::new(ffmpeg_path())
669 .create_no_window()
670 .arg("-version")
671 .stdout(Stdio::piped())
672 .spawn()
674 .unwrap();
675
676 let stdout = cmd.stdout.unwrap();
677 let mut parser = FfmpegLogParser::new(stdout);
678 while let Ok(event) = parser.parse_next_event() {
679 if let FfmpegEvent::ParsedConfiguration(_) = event {
680 return;
681 }
682 }
683 panic!() }
685
686 #[test]
688 fn test_macos_line_endings() {
689 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";
690
691 let mut cursor = Cursor::new(Vec::new());
693 cursor.write_all(stdout_str.as_bytes()).unwrap();
694 cursor.seek(SeekFrom::Start(0)).unwrap();
695
696 let reader = BufReader::new(cursor);
697 let mut parser = FfmpegLogParser::new(reader);
698 let mut num_events = 0;
699 while let Ok(event) = parser.parse_next_event() {
700 match event {
701 FfmpegEvent::LogEOF => break,
702 _ => num_events += 1,
703 }
704 }
705 assert!(num_events > 1);
706 }
707
708 #[test]
712 fn test_parse_progress_v7() {
713 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";
714 let progress = try_parse_progress(line).unwrap();
715 assert!(progress.frame == 5);
716 assert!(progress.fps == 0.0);
717 assert!(progress.q == -1.0);
718 assert!(progress.size_kb == 10);
719 assert!(progress.time == "00:00:03.00");
720 assert!(progress.bitrate_kbps == 27.2);
721 assert!(progress.speed == 283.0);
722 }
723
724 #[test]
727 fn test_parse_progress_empty() {
728 let line =
729 "[info] frame= 0 fps=0.0 q=-0.0 size= 0kB time=00:00:00.00 bitrate=N/A speed=N/A\n";
730 let progress = try_parse_progress(line).unwrap();
731 assert!(progress.frame == 0);
732 assert!(progress.fps == 0.0);
733 assert!(progress.q == -0.0);
734 assert!(progress.size_kb == 0);
735 assert!(progress.time == "00:00:00.00");
736 assert!(progress.bitrate_kbps == 0.0);
737 assert!(progress.speed == 0.0);
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}