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

Issue:
FFmpeg's nearly unlimited output causes ffmpeg-sidecar to hang without a timeout
I'm converting a video(with some strange .ts extension) to a 3-seconds gif using ffmpeg-sidecar. Command like

```
ffmpeg -loglevel level+warning -hide_banner -i "/path/to/bipbop-gear1-all.ts" -t 3 -an -vf "fps=10,scale=-1:200:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=164:stats_mode=diff[p];[s1][p]paletteuse=dither=floyd_steinberg:diff_mode=rectangle" -compression_level 9 -quality 85 -c:v gif -y "/path/to/thumbnail.gif"
```

However, ffmpeg cli output nearly unlimited lines of warning, like 

```
[Parsed_palettegen_4 @ 0x600001574bb0] [warning] The input frame is not in sRGB, colors may be off
[Parsed_palettegen_4 @ 0x600001574bb0] [warning] The input frame is not in sRGB, colors may be off
...(tons of lines...)
[Parsed_palettegen_4 @ 0x600001574bb0] [warning] The input frame is not in sRGB, colors may be off
[Parsed_palettegen_4 @ 0x600001574bb0] [warning] The input frame is not in sRGB, colors may be off
```

but, ffmpeg cli just **exits(not sure exit 0 or not 0) after about ~7 seconds**, which I think reasonable.

When it comes to ffmpeg-sidecar in Rust

```
    let exit_status = FfmpegCommand::new()
        .args([
            "-loglevel", "level+warning",
            "-hide_banner",
            "-i", filepath,
            "-t", "3",
            "-an",
            "-vf", "fps=10,scale=-1:200:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=164:stats_mode=diff[p];[s1][p]paletteuse=dither=floyd_steinberg:diff_mode=rectangle",
            "-compression_level", "9",
            "-quality", "85",
            "-c:v", "gif",
            "-y",
            &thumbnail_path_buf.to_string_lossy()
        ])
        .spawn()?
        .wait()?;

    if exit_status.success() {
        Ok(...)
    } else {
        Err(...)
    }
```

FfmpegCommand(FfmpegChild maybe) thread just hangs at .wait()?

Maybe adding timeout parameter would be better? like the ffmpeg cli do internally? Or, I have to set a timer, and invoke handle FfmpegChild.quit/kill(maybe) when timeout, not very convenient.

You can download the test video at [http://www.live555.com/liveMedia/public/h264-in-mp2t/bipbop-gear1-all.ts](http://www.live555.com/liveMedia/public/h264-in-mp2t/bipbop-gear1-all.ts)


Discussion / hints:
Thanks for providing a test video, I will take a look.

Fix patch:
diff --git a/src/child.rs b/src/child.rs
index f252ded..a4c3b7d 100644
--- a/src/child.rs
+++ b/src/child.rs
@@ -1,14 +1,12 @@
 //! Wrapper around `std::process::Child` containing a spawned FFmpeg command.
 
+use crate::iter::FfmpegIterator;
+use anyhow::Context;
 use std::{
-  io::{self, Write},
+  io::{self, copy, sink, Write},
   process::{Child, ChildStderr, ChildStdin, ChildStdout, ExitStatus},
 };
 
-use anyhow::Context;
-
-use crate::iter::FfmpegIterator;
-
 /// A wrapper around [`std::process::Child`] containing a spawned FFmpeg command.
 /// Provides interfaces for reading parsed metadata, progress updates, warnings and errors, and
 /// piped output frames if applicable.
@@ -100,6 +98,12 @@ impl FfmpegChild {
   ///
   /// Identical to `wait` in [`std::process::Child`].
   pub fn wait(&mut self) -> io::Result<ExitStatus> {
+    // If stderr hasn't already been consumed by a method like `iter()`,
+    // we need to run it to completion to avoid a deadlock.
+    if let Some(mut stderr) = self.take_stderr() {
+      copy(&mut stderr, &mut sink())?;
+    };
+
     self.inner.wait()
   }
 


Test patch:
diff --git a/src/test.rs b/src/test.rs
index 2cd591e..a9ea3f3 100644
--- a/src/test.rs
+++ b/src/test.rs
@@ -40,6 +40,35 @@ fn spawn_with_timeout(command: &mut FfmpegCommand, timeout: u64) -> anyhow::Resu
   }
 }
 
+/// Returns `Err` if the timeout thread finishes before the FFmpeg process
+/// Note: this variant leaves behind a hung FFmpeg child process + thread until
+/// the test suite exits.
+fn wait_with_timeout(command: &mut FfmpegCommand, timeout: u64) -> anyhow::Result<()> {
+  let (sender, receiver) = mpsc::channel();
+
+  // Thread 1: Waits for 1000ms and sends a message
+  let timeout_sender = sender.clone();
+  thread::spawn(move || {
+    thread::sleep(Duration::from_millis(timeout));
+    timeout_sender.send("timeout").ok();
+  });
+
+  // Thread 2: Wait for the child to exit in another thread
+  let mut ffmpeg_child = command.spawn()?;
+  thread::spawn(move || {
+    ffmpeg_child.wait().unwrap();
+    sender.send("ffmpeg").ok();
+  });
+
+  // Race the two threads
+  let finished_first = receiver.recv()?;
+  match finished_first {
+    "timeout" => anyhow::bail!("Timeout thread expired before FFmpeg"),
+    "ffmpeg" => Ok(()),
+    _ => anyhow::bail!("Unknown message received"),
+  }
+}
+
 #[test]
 fn test_installed() {
   assert!(ffmpeg_is_installed());
@@ -715,3 +744,30 @@ fn test_no_empty_events() -> anyhow::Result<()> {
 
   Ok(())
 }
+
+/// This command generates an warning on every frame, e.g.:
+///
+/// ```txt
+/// [Parsed_palettegen_4 @ 0x600001574bb0] [warning] The input frame is not in sRGB, colors may be off
+///```
+///
+/// When used in combination with `.wait()`, these error messages can completely
+/// fill the stderr buffer and cause a deadlock. The solution is to
+/// automatically drop the stderr channel when `.wait()` is called.
+///
+/// <https://github.com/nathanbabcock/ffmpeg-sidecar/issues/70>
+#[test]
+fn test_wait() -> anyhow::Result<()> {
+  let mut command = FfmpegCommand::new();
+  command
+    .args("-color_primaries 1".split(' '))
+    .args("-color_trc 1".split(' '))
+    .args("-colorspace 1".split(' '))
+    .format("lavfi")
+    .input("yuvtestsrc=size=64x64:rate=60:duration=60")
+    .args("-vf palettegen=max_colors=164".split(' '))
+    .codec_video("gif")
+    .format("null")
+    .output("-");
+  wait_with_timeout(&mut command, 5000)
+}