1use crate::comma_iter::CommaIter;
5use crate::event::{
6 AudioStream, FfmpegConfiguration, FfmpegDuration, FfmpegEvent, FfmpegInput, FfmpegOutput,
7 FfmpegProgress, FfmpegStream, FfmpegVersion, LogLevel, StreamTypeSpecificData, VideoStream,
8};
9use tokio::io::{AsyncBufRead, AsyncBufReadExt, BufReader, Lines};
10
11#[derive(Debug, Clone, PartialEq)]
12enum LogSection {
13 Input(u32),
14 Output(u32),
15 StreamMapping,
16 Other,
17}
18
19pub struct FfmpegLogParser<R: AsyncBufRead + Unpin> {
20 lines: Lines<BufReader<R>>,
21 cur_section: LogSection,
22}
23
24impl<R: AsyncBufRead + Unpin> FfmpegLogParser<R> {
25 pub async fn parse_next_event(&mut self) -> anyhow::Result<FfmpegEvent> {
37 let Some(line) = self.lines.next_line().await? else {
38 return Ok(FfmpegEvent::LogEOF);
39 };
40
41 let raw_log_message = line.clone();
43
44 if let Some(input_number) = try_parse_input(&line) {
46 self.cur_section = LogSection::Input(input_number);
47 return Ok(FfmpegEvent::ParsedInput(FfmpegInput {
48 index: input_number,
49 duration: None,
50 raw_log_message,
51 }));
52 } else if let Some(output) = try_parse_output(&line) {
53 self.cur_section = LogSection::Output(output.index);
54 return Ok(FfmpegEvent::ParsedOutput(output));
55 } else if line.contains("Stream mapping:") {
56 self.cur_section = LogSection::StreamMapping;
57 }
58
59 if let Some(version) = try_parse_version(&line) {
61 Ok(FfmpegEvent::ParsedVersion(FfmpegVersion {
62 version,
63 raw_log_message,
64 }))
65 } else if let Some(configuration) = try_parse_configuration(&line) {
66 Ok(FfmpegEvent::ParsedConfiguration(FfmpegConfiguration {
67 configuration,
68 raw_log_message,
69 }))
70 } else if let Some(duration) = try_parse_duration(&line) {
71 match self.cur_section {
72 LogSection::Input(input_index) => Ok(FfmpegEvent::ParsedDuration(FfmpegDuration {
73 input_index,
74 duration,
75 raw_log_message,
76 })),
77 _ => Ok(FfmpegEvent::Log(LogLevel::Info, line.to_string())),
78 }
79 } else if self.cur_section == LogSection::StreamMapping && line.contains(" Stream #") {
80 Ok(FfmpegEvent::ParsedStreamMapping(line.to_string()))
81 } else if let Some(stream) = try_parse_stream(&line) {
82 match self.cur_section {
83 LogSection::Input(_) => Ok(FfmpegEvent::ParsedInputStream(stream)),
84 LogSection::Output(_) => Ok(FfmpegEvent::ParsedOutputStream(stream)),
85 LogSection::Other | LogSection::StreamMapping => Err(anyhow::Error::msg(format!(
86 "Unexpected stream specification: {}",
87 line
88 ))),
89 }
90 } else if let Some(progress) = try_parse_progress(&line) {
91 self.cur_section = LogSection::Other;
92 Ok(FfmpegEvent::Progress(progress))
93 } else if line.contains("[info]") {
94 Ok(FfmpegEvent::Log(LogLevel::Info, line.to_string()))
95 } else if line.contains("[warning]") {
96 Ok(FfmpegEvent::Log(LogLevel::Warning, line.to_string()))
97 } else if line.contains("[error]") {
98 Ok(FfmpegEvent::Log(LogLevel::Error, line.to_string()))
99 } else if line.contains("[fatal]") {
100 Ok(FfmpegEvent::Log(LogLevel::Fatal, line.to_string()))
101 } else {
102 Ok(FfmpegEvent::Log(LogLevel::Unknown, line.to_string()))
103 }
104 }
105
106 pub fn new(inner: R) -> Self {
107 let buf_read = BufReader::new(inner);
108 let lines = buf_read.lines();
109
110 Self {
111 lines,
112 cur_section: LogSection::Other,
113 }
114 }
115}
116
117pub fn try_parse_version(string: &str) -> Option<String> {
130 string
131 .strip_prefix("[info]")
132 .unwrap_or(string)
133 .trim()
134 .strip_prefix("ffmpeg version ")?
135 .split_whitespace()
136 .next()
137 .map(|s| s.to_string())
138}
139
140pub fn try_parse_configuration(string: &str) -> Option<Vec<String>> {
160 string
161 .strip_prefix("[info]")
162 .unwrap_or(string)
163 .trim()
164 .strip_prefix("configuration: ")
165 .map(|s| s.split_whitespace().map(|s| s.to_string()).collect())
166}
167
168pub fn try_parse_input(string: &str) -> Option<u32> {
179 string
180 .strip_prefix("[info]")
181 .unwrap_or(string)
182 .trim()
183 .strip_prefix("Input #")?
184 .split_whitespace()
185 .next()
186 .and_then(|s| s.split(',').next())
187 .and_then(|s| s.parse::<u32>().ok())
188}
189
190pub fn try_parse_duration(string: &str) -> Option<f64> {
208 string
209 .strip_prefix("[info]")
210 .unwrap_or(string)
211 .trim()
212 .strip_prefix("Duration:")?
213 .trim()
214 .split(',')
215 .next()
216 .and_then(parse_time_str)
217}
218
219pub fn try_parse_output(mut s: &str) -> Option<FfmpegOutput> {
235 let raw_log_message = s.to_string();
236
237 s = s
238 .strip_prefix("[info]")
239 .unwrap_or(s)
240 .trim()
241 .strip_prefix("Output #")?;
242
243 let index = s
244 .split_whitespace()
245 .next()
246 .and_then(|s| s.split(',').next())
247 .and_then(|s| s.parse::<u32>().ok())?;
248
249 let to = s.split(" to '").nth(1)?.split('\'').next()?.to_string();
250
251 Some(FfmpegOutput {
252 index,
253 to,
254 raw_log_message,
255 })
256}
257
258pub fn try_parse_stream(s: &str) -> Option<FfmpegStream> {
400 let raw_log_message = s.to_string();
401
402 let s = s
403 .strip_prefix("[info]")
404 .unwrap_or(s)
405 .trim()
406 .strip_prefix("Stream #")?;
407 let mut comma_iter = CommaIter::new(s);
408 let mut colon_iter = comma_iter.next()?.split(':');
409
410 let parent_index = colon_iter.next()?.parse::<u32>().ok()?;
411
412 let indices_nad_maybe_language = colon_iter
414 .next()?
415 .split(['[', ']'])
417 .step_by(2)
418 .collect::<String>();
419 let mut paranthesis_iter = indices_nad_maybe_language.split('(');
420 let stream_index = paranthesis_iter.next()?.trim().parse::<u32>().ok()?;
421 let language = paranthesis_iter.next().map_or("".to_string(), |lang| {
422 lang.trim_end_matches(')').to_string()
423 });
424
425 let stream_type = colon_iter.next()?.trim();
427 let format = colon_iter
428 .next()?
429 .trim()
430 .split(&[' ', '(']) .next()?
432 .to_string();
433
434 let type_specific_data: StreamTypeSpecificData = match stream_type {
436 "Audio" => try_parse_audio_stream(comma_iter)?,
437 "Subtitle" => StreamTypeSpecificData::Subtitle,
438 "Video" => try_parse_video_stream(comma_iter)?,
439 _ => StreamTypeSpecificData::Other,
440 };
441
442 Some(FfmpegStream {
443 format,
444 language,
445 parent_index,
446 stream_index,
447 raw_log_message,
448 type_specific_data,
449 })
450}
451
452fn try_parse_audio_stream(mut comma_iter: CommaIter) -> Option<StreamTypeSpecificData> {
454 let sample_rate = comma_iter
455 .next()?
456 .split_whitespace()
457 .next()?
458 .parse::<u32>()
459 .ok()?;
460
461 let channels = comma_iter.next()?.trim().to_string();
462
463 Some(StreamTypeSpecificData::Audio(AudioStream {
464 sample_rate,
465 channels,
466 }))
467}
468
469fn try_parse_video_stream(mut comma_iter: CommaIter) -> Option<StreamTypeSpecificData> {
471 let pix_fmt = comma_iter
472 .next()?
473 .trim()
474 .split(&[' ', '(']) .next()?
476 .to_string();
477
478 let dims = comma_iter.next()?.split_whitespace().next()?;
479 let mut dims_iter = dims.split('x');
480 let width = dims_iter.next()?.parse::<u32>().ok()?;
481 let height = dims_iter.next()?.parse::<u32>().ok()?;
482
483 let fps = comma_iter
486 .find_map(|part| {
487 if part.trim().ends_with("fps") {
488 part.split_whitespace().next()
489 } else {
490 None
491 }
492 })
493 .and_then(|fps_str| fps_str.parse::<f32>().ok())?;
494
495 Some(StreamTypeSpecificData::Video(VideoStream {
496 pix_fmt,
497 width,
498 height,
499 fps,
500 }))
501}
502
503pub fn try_parse_progress(mut string: &str) -> Option<FfmpegProgress> {
519 let raw_log_message = string.to_string();
520
521 string = string.strip_prefix("[info]").unwrap_or(string).trim();
522
523 let frame = string
524 .split("frame=")
525 .nth(1)?
526 .split_whitespace()
527 .next()?
528 .parse::<u32>()
529 .ok()?;
530 let fps = string
531 .split("fps=")
532 .nth(1)?
533 .split_whitespace()
534 .next()?
535 .parse::<f32>()
536 .ok()?;
537 let q = string
538 .split("q=")
539 .nth(1)?
540 .split_whitespace()
541 .next()?
542 .parse::<f32>()
543 .ok()?;
544 let size_kb = string
545 .split("size=") .nth(1)?
547 .split_whitespace()
548 .next()
549 .map(|s| s.trim())
550 .and_then(|s| {
551 s.strip_suffix("KiB") .or_else(|| s.strip_suffix("kB")) .or_else(|| s.ends_with("N/A").then(|| "0")) })?
555 .parse::<u32>()
556 .ok()?;
557 let time = string
558 .split("time=")
559 .nth(1)?
560 .split_whitespace()
561 .next()?
562 .to_string();
563 let bitrate_kbps = string
564 .split("bitrate=")
565 .nth(1)?
566 .split_whitespace()
567 .next()?
568 .trim()
569 .replace("kbits/s", "")
570 .parse::<f32>()
571 .unwrap_or(0.0); let speed = string
573 .split("speed=")
574 .nth(1)?
575 .split_whitespace()
576 .next()?
577 .strip_suffix('x')
578 .map(|s| s.parse::<f32>().unwrap_or(0.0))
579 .unwrap_or(0.0);
580
581 Some(FfmpegProgress {
582 frame,
583 fps,
584 q,
585 size_kb,
586 time,
587 bitrate_kbps,
588 speed,
589 raw_log_message,
590 })
591}
592
593pub fn parse_time_str(s: &str) -> Option<f64> {
610 let mut seconds = 0.0;
611
612 let mut smh = s.split(':').rev();
613 if let Some(sec) = smh.next() {
614 seconds += sec.parse::<f64>().ok()?;
615 }
616
617 if let Some(min) = smh.next() {
618 seconds += min.parse::<f64>().ok()? * 60.0;
619 }
620
621 if let Some(hrs) = smh.next() {
622 seconds += hrs.parse::<f64>().ok()? * 60.0 * 60.0;
623 }
624
625 Some(seconds)
626}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631 use crate::command::BackgroundCommand;
633 use crate::paths::ffmpeg_path;
634 use std::io::{Cursor, Seek, SeekFrom, Write};
635 use std::process::Stdio;
636 use tokio::process::Command;
637
638 #[tokio::test]
639 async fn test_parse_version() {
640 let cmd = Command::new(ffmpeg_path())
641 .create_no_window()
642 .arg("-version")
643 .stdout(Stdio::piped())
644 .spawn()
646 .unwrap();
647
648 let stdout = cmd.stdout.unwrap();
649 let reader = BufReader::new(stdout);
650 let mut parser = FfmpegLogParser::new(reader);
651 while let Ok(event) = parser.parse_next_event().await {
652 if let FfmpegEvent::ParsedVersion(_) = event {
653 return;
654 }
655 }
656 panic!() }
658
659 #[tokio::test]
660 async fn test_parse_configuration() {
661 let cmd = Command::new(ffmpeg_path())
662 .create_no_window()
663 .arg("-version")
664 .stdout(Stdio::piped())
665 .spawn()
667 .unwrap();
668
669 let stdout = cmd.stdout.unwrap();
670 let reader = BufReader::new(stdout);
671 let mut parser = FfmpegLogParser::new(reader);
672 while let Ok(event) = parser.parse_next_event().await {
673 if let FfmpegEvent::ParsedConfiguration(_) = event {
674 return;
675 }
676 }
677 panic!() }
679
680 #[tokio::test]
682 async fn test_macos_line_endings() {
683 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";
684
685 let mut cursor = Cursor::new(Vec::new());
687 cursor.write_all(stdout_str.as_bytes()).unwrap();
688 cursor.seek(SeekFrom::Start(0)).unwrap();
689
690 let reader = BufReader::new(cursor);
691 let mut parser = FfmpegLogParser::new(reader);
692 let mut num_events = 0;
693 while let Ok(event) = parser.parse_next_event().await {
694 match event {
695 FfmpegEvent::LogEOF => break,
696 _ => num_events += 1,
697 }
698 }
699 assert!(num_events > 1);
700 }
701
702 #[test]
706 fn test_parse_progress_v7() {
707 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";
708 let progress = try_parse_progress(line).unwrap();
709 assert_eq!(progress.frame, 5);
710 assert_eq!(progress.fps, 0.0);
711 assert_eq!(progress.q, -1.0);
712 assert_eq!(progress.size_kb, 10);
713 assert_eq!(progress.time, "00:00:03.00");
714 assert_eq!(progress.bitrate_kbps, 27.2);
715 assert_eq!(progress.speed, 283.0);
716 }
717
718 #[test]
719 fn test_parse_progress_no_size() {
720 let line = "[info] frame= 163 fps= 13 q=4.4 size=N/A time=00:13:35.00 bitrate=N/A speed=64.7x";
721 let progress = try_parse_progress(line).unwrap();
722 assert!(progress.frame == 163);
723 assert!(progress.fps == 13.0);
724 assert!(progress.q == 4.4);
725 assert!(progress.size_kb == 0);
726 assert!(progress.time == "00:13:35.00");
727 assert!(progress.bitrate_kbps == 0.0);
728 assert!(progress.speed == 64.7);
729 }
730
731 #[test]
734 fn test_parse_progress_empty() {
735 let line =
736 "[info] frame= 0 fps=0.0 q=-0.0 size= 0kB time=00:00:00.00 bitrate=N/A speed=N/A\n";
737 let progress = try_parse_progress(line).unwrap();
738 assert_eq!(progress.frame, 0);
739 assert_eq!(progress.fps, 0.0);
740 assert_eq!(progress.q, -0.0);
741 assert_eq!(progress.size_kb, 0);
742 assert_eq!(progress.time, "00:00:00.00");
743 assert_eq!(progress.bitrate_kbps, 0.0);
744 assert_eq!(progress.speed, 0.0);
745 }
746}