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

Issue:
Garbled output from FFmpeg caused an FFmpeg-sidecar panic
Some video(unzip attachment) has non-ascii characters in ffmpeg output, which cause ffmpeg-sidecar panic, with console output  `Error parsing ffmpeg output: invalid utf-8 sequence of 1 bytes from index 39`). It might be better to **return an Error** instead of panic?

The raw command line output looks like below. See the handler_name part.

```
./ffmpeg-aarch64-apple-darwin \                                                                                                                                                              git:(master|)
  -loglevel level+info \
  -hide_banner \
  -i /path/to/jnet_sample_100k.3g2 \
  -an \
  -frames:v 1 \
  -pix_fmt yuvj444p \
  -vf scale=-1:220 \
  -fps_mode vfr \
  -update 1 \
  -y "/path/to/thumbnail.jpg"
[info] Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/Users/wikke/Pictures/dataset/video/20241203/mplayerhq/mobileVideo_3gp/jnet_sample_100k.3g2':
[info]   Metadata:
[info]     major_brand     : 3g2a
[info]     minor_version   : 65536
[info]     compatible_brands: 3g2amp42
[info]     creation_time   : 2004-06-03T15:01:10.000000Z
[info]   Duration: 00:01:29.53, start: 0.000000, bitrate: 112 kb/s
[info]   Stream #0:0[0x1](eng): Audio: amr_nb (amrnb) (samr / 0x726D6173), 8000 Hz, mono, fltp, 12 kb/s (default)
[info]       Metadata:
[info]         creation_time   : 2004-06-03T15:01:10.000000Z
[info]         handler_name    : Apple �T�E���h�E���f�B�A�n���h��
[info]         vendor_id       : [0][0][0][0]
[info]   Stream #0:1[0x2](eng): Video: mpeg4 (Simple Profile) (mp4v / 0x7634706D), yuv420p(pc, smpte170m/unknown/smpte170m), 176x144 [SAR 1:1 DAR 11:9], 98 kb/s, 15 fps, 15 tbr, 30k tbn (default)
[info]       Metadata:
[info]         creation_time   : 2004-06-03T15:01:10.000000Z
[info]         handler_name    : Apple �r�f�I�E���f�B�A�n���h��
[info]         vendor_id       : [0][0][0][0]
[info] Stream mapping:
[info]   Stream #0:1 -> #0:0 (mpeg4 (native) -> mjpeg (native))
[info] Press [q] to stop, [?] for help
[swscaler @ 0x158028000] [warning] deprecated pixel format used, make sure you did set range correctly
[info] Output #0, image2, to '/Users/wikke/Library/Application Support/acs/thumbnails/0b/3e/0b3ec4f372d84d4ea5ca76c7ea09f13a-mannual.jpg':
[info]   Metadata:
[info]     major_brand     : 3g2a
[info]     minor_version   : 65536
[info]     compatible_brands: 3g2amp42
[info]     encoder         : Lavf61.7.100
[info]   Stream #0:0(eng): Video: mjpeg, yuvj444p(pc, smpte170m/unknown/smpte170m, progressive), 269x220 [SAR 2420:2421 DAR 11:9], q=2-31, 200 kb/s, 15 fps, 15 tbn (default)
[info]       Metadata:
[info]         creation_time   : 2004-06-03T15:01:10.000000Z
[info]         handler_name    : Apple �r�f�I�E���f�B�A�n���h��
[info]         vendor_id       : [0][0][0][0]
[info]         encoder         : Lavc61.19.100 mjpeg
[info]       Side data:
[info]         cpb: bitrate max/min/avg: 0/0/200000 buffer size: 0 vbv_delay: N/A
[out#0/image2 @ 0x60000280c000] [info] video:1KiB audio:0KiB subtitle:0KiB other streams:0KiB global headers:0KiB muxing overhead: unknown
[info] frame=    1 fps=0.0 q=1.6 Lsize=N/A time=00:00:00.06 bitrate=N/A speed=10.8x
```

And the rust code

```
    FfmpegCommand::new()
        .args([
            "-hide_banner",
            "-i", input,
            "-an",
            "-frames:v", "1",
            "-pix_fmt", "yuvj444p",
            "-vf", "scale=-1:220",
            "-fps_mode", "vfr",
            // "-hwaccel", "auto",
            "-update", "1",
            "-y",
            &thumbnail_path_buf.to_string_lossy()
        ])
        // .print_command()
        .spawn()?
        .iter()
        .map_err(|_| acs_err("ffmpeg error"))?
        .for_each(ffmpeg_event_display);
```

and

```
pub fn ffmpeg_event_display(e: FfmpegEvent) {
    match e {
        FfmpegEvent::Progress(FfmpegProgress { frame, fps, q, size_kb, time, bitrate_kbps, speed, raw_log_message }) =>
            log::debug!("Current frame:{frame}, fps:{fps}, q:{q}, size_kb:{size_kb}, time:{time}, bitrate_kbps:{bitrate_kbps}, speed:{speed}, raw_log_message:{raw_log_message}"),
        FfmpegEvent::Log(LogLevel::Warning, msg) => {
            if !msg.is_empty() {
                log::warn!("warn:{}", msg.trim())
            }
        }
        FfmpegEvent::Log(LogLevel::Error | LogLevel::Fatal, msg) => {
            if !msg.is_empty() {
                log::error!("error:{}", msg.trim())
            }
        }
        _ => {}
    }
}
```

[jnet_sample_100k.3g2.zip](https://github.com/user-attachments/files/18011244/jnet_sample_100k.3g2.zip)



Discussion / hints:
Thanks, I'm taking a look at this

Fix patch:
diff --git a/src/log_parser.rs b/src/log_parser.rs
index c049953..b7cf53d 100644
--- a/src/log_parser.rs
+++ b/src/log_parser.rs
@@ -1,9 +1,6 @@
 //! Internal methods for parsing FFmpeg CLI log output.
 
-use std::{
-  io::{BufReader, Read},
-  str::from_utf8,
-};
+use std::io::{BufReader, Read};
 
 use crate::{
   comma_iter::CommaIter,
@@ -42,7 +39,8 @@ impl<R: Read> FfmpegLogParser<R> {
   pub fn parse_next_event(&mut self) -> anyhow::Result<FfmpegEvent> {
     let mut buf = Vec::<u8>::new();
     let bytes_read = read_until_any(&mut self.reader, &[b'\r', b'\n'], &mut buf);
-    let line = from_utf8(buf.as_slice())?.trim();
+    let line_cow = String::from_utf8_lossy(buf.as_slice());
+    let line = line_cow.trim();
     let raw_log_message = line.to_string();
     match bytes_read? {
       0 => Ok(FfmpegEvent::LogEOF),
@@ -738,4 +736,21 @@ mod tests {
     assert!(progress.bitrate_kbps == 0.0);
     assert!(progress.speed == 0.0);
   }
+
+  /// Coverage for non-utf-8 bytes: https://github.com/nathanbabcock/ffmpeg-sidecar/issues/67
+  #[test]
+  fn test_non_utf8() -> anyhow::Result<()> {
+    // Create a dummy stderr stream with non-utf8 bytes
+    let mut cursor = Cursor::new(Vec::new());
+    cursor
+      .write_all(b"[info] \x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\n")
+      .unwrap();
+    cursor.seek(SeekFrom::Start(0)).unwrap();
+
+    let event = FfmpegLogParser::new(cursor).parse_next_event()?;
+
+    assert!(matches!(event, FfmpegEvent::Log(LogLevel::Info, _)));
+
+    Ok(())
+  }
 }


Test patch: