Repository: nathanbabcock/ffmpeg-sidecar (language: rust)

Issue:
Issue running on MacOS
I suspect there is an issue with parsing the `ffmpeg` output on MacOS. When running the examples, execution gets blocked at creating the `FfmpegIterator`. I did a little digging and it turns out only one event is received from the thread that parses the `ffmpeg` output and then it hangs:
```
[src/iter.rs:35] &event = ParsedVersion(
    FfmpegVersion {
        version: "N-109875-geabc304d12-tessus",
        raw_log_message: "[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",
    },
)
```
Compared to the debug output on Windows it looks it parses the complete output as a `ParsedVersion`. Maybe something to do with line endings?


Discussion / hints:
@avsaase thanks for the bug report and the debug output. I never tested on Mac and I think you're right, it must be the line endings. I'll publish a fix shortly.

Fix patch:
diff --git a/src/lib.rs b/src/lib.rs
index 6d5e995..c999135 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -30,6 +30,9 @@
 //! ```
 //!
 
+#[cfg(test)]
+mod test;
+
 pub mod child;
 pub mod comma_iter;
 pub mod command;
@@ -38,6 +41,4 @@ pub mod event;
 pub mod iter;
 pub mod log_parser;
 pub mod pix_fmt;
-
-#[cfg(test)]
-mod test;
+pub mod read_until_any;
diff --git a/src/log_parser.rs b/src/log_parser.rs
index 5da71c7..328ba51 100644
--- a/src/log_parser.rs
+++ b/src/log_parser.rs
@@ -1,5 +1,5 @@
 use std::{
-  io::{BufRead, BufReader, Read},
+  io::{BufReader, Read},
   str::from_utf8,
 };
 
@@ -9,6 +9,7 @@ use crate::{
   event::{
     AVStream, FfmpegConfiguration, FfmpegEvent, FfmpegOutput, FfmpegProgress, FfmpegVersion,
   },
+  read_until_any::read_until_any,
 };
 
 #[derive(Debug, Clone, PartialEq)]
@@ -31,9 +32,14 @@ impl<R: Read> FfmpegLogParser<R> {
   /// Typically this consumes a single line, but in the case of multi-line
   /// input/output stream specifications, nested method calls will consume
   /// additional lines until the entire vector of Inputs/Outputs is parsed.
+  ///
+  /// Line endings can be marked by three possible delimiters:
+  /// - `\n` (MacOS),
+  /// - `\r\n` (Windows)
+  /// - `\r` (Windows, progress updates which overwrite the previous line)
   pub fn parse_next_event(&mut self) -> Result<FfmpegEvent> {
     let mut buf = Vec::<u8>::new();
-    let bytes_read = self.reader.read_until(b'\r', &mut buf);
+    let bytes_read = read_until_any(&mut self.reader, &[b'\r', b'\n'], &mut buf);
     let line = from_utf8(buf.as_slice())?.trim();
     match bytes_read {
       Ok(0) => Ok(FfmpegEvent::LogEOF),
@@ -376,7 +382,10 @@ pub fn try_parse_progress(mut string: &str) -> Option<FfmpegProgress> {
 #[cfg(test)]
 mod tests {
   use super::*;
-  use std::process::{Command, Stdio};
+  use std::{
+    io::{Cursor, Seek, SeekFrom, Write},
+    process::{Command, Stdio},
+  };
 
   #[test]
   fn test_version() {
@@ -417,4 +426,26 @@ mod tests {
     }
     panic!() // should have found a configuration
   }
+
+  /// Test case from https://github.com/nathanbabcock/ffmpeg-sidecar/issues/2#issue-1606661255
+  #[test]
+  fn test_macos_line_endings() {
+    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";
+
+    // Emulate a stderr channel
+    let mut cursor = Cursor::new(Vec::new());
+    cursor.write_all(stdout_str.as_bytes()).unwrap();
+    cursor.seek(SeekFrom::Start(0)).unwrap();
+
+    let reader = BufReader::new(cursor);
+    let mut parser = FfmpegLogParser::new(reader);
+    let mut num_events = 0;
+    while let Ok(event) = parser.parse_next_event() {
+      match event {
+        FfmpegEvent::LogEOF => break,
+        _ => num_events += 1,
+      }
+    }
+    assert!(num_events > 1);
+  }
 }
diff --git a/src/read_until_any.rs b/src/read_until_any.rs
new file mode 100644
index 0000000..febfefd
--- /dev/null
+++ b/src/read_until_any.rs
@@ -0,0 +1,40 @@
+use std::io::{BufRead, ErrorKind, Result};
+
+/// `BufRead::read_until` with multiple delimiters.
+pub fn read_until_any<R: BufRead + ?Sized>(
+  r: &mut R,
+  delims: &[u8],
+  buf: &mut Vec<u8>,
+) -> Result<usize> {
+  let mut read = 0;
+  loop {
+    let (done, used) = {
+      let available = match r.fill_buf() {
+        Ok(n) => n,
+        Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
+        Err(e) => return Err(e),
+      };
+
+      // NB: `memchr` crate would be faster, but it's unstable and not worth the dependency.
+      let first_delim_index = available
+        .iter()
+        .position(|b| delims.iter().find(|d| **d == *b).is_some());
+
+      match first_delim_index {
+        Some(i) => {
+          buf.extend_from_slice(&available[..=i]);
+          (true, i + 1)
+        }
+        None => {
+          buf.extend_from_slice(available);
+          (false, available.len())
+        }
+      }
+    };
+    r.consume(used);
+    read += used;
+    if done || used == 0 {
+      return Ok(read);
+    }
+  }
+}


Test patch: