diff --git a/CHANGELOG.md b/CHANGELOG.md index cc0e005..8f31678 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +* Fix VMAF score parse failure of certain successful ffmpeg outputs. + # v0.7.15 * Show full ffmpeg command after errors. * For *_vaapi encoders map `--crf` to ffmpeg `-q` (instead of `-qp`). diff --git a/Cargo.lock b/Cargo.lock index 2e2f8e4..3811b18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,9 +195,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490" +checksum = "5208975e568d83b6b05cc0a063c8e7e9acc2b43bee6da15616a5b73e109d7437" [[package]] name = "cfg-if" @@ -798,18 +798,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", @@ -818,9 +818,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.119" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8eddb61f0697cc3989c5d64b452f5488e2b8a60fd7d5076a3045076ffef8cb0" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", @@ -881,9 +881,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.68" +version = "2.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +checksum = "201fcda3845c23e8212cd466bfebf0bd20694490fc0356ae8e428e0824a915a6" dependencies = [ "proc-macro2", "quote", @@ -1078,7 +1078,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -1098,18 +1098,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -1120,9 +1120,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -1132,9 +1132,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -1144,15 +1144,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -1162,9 +1162,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -1174,9 +1174,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -1186,9 +1186,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -1198,6 +1198,6 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/src/process.rs b/src/process.rs index 0885cb3..77f0083 100644 --- a/src/process.rs +++ b/src/process.rs @@ -52,7 +52,7 @@ pub fn exit_ok_stderr( pub fn cmd_err(err: impl Display, cmd_str: &str, stderr: &Chunks) -> anyhow::Error { anyhow!( "{err}\n----cmd-----\n{cmd_str}\n---stderr---\n{}\n------------", - stderr.out.trim() + String::from_utf8_lossy(&stderr.out).trim() ) } @@ -148,7 +148,7 @@ fn parse_label_size(label: &str, line: &str) -> Option { /// Stores up to ~4k chunk data on the heap. #[derive(Default)] pub struct Chunks { - out: String, + out: Vec, } impl Chunks { @@ -156,36 +156,47 @@ impl Chunks { pub fn push(&mut self, chunk: &[u8]) { const MAX_LEN: usize = 4000; - self.out.push_str(&String::from_utf8_lossy(chunk)); + self.out.extend(chunk); // truncate beginning if too long - let len = self.out.len(); - if len > MAX_LEN + 100 { - self.out = String::from_utf8_lossy(&self.out.as_bytes()[len - MAX_LEN..]).into(); + if self.out.len() > MAX_LEN + 100 { + // remove lines until small + while self.out.len() > MAX_LEN { + let mut next_eol = self + .out + .iter() + .position(|b| *b == b'\n') + .unwrap_or(self.out.len() - 1); + if self.out.get(next_eol + 1) == Some(&b'\r') { + next_eol += 1; + } + + self.out.splice(..next_eol + 1, []); + } } } - fn rlines(&self) -> impl Iterator { - self.out - .rsplit_terminator('\n') - .flat_map(|l| l.rsplit_terminator('\r')) + pub fn rfind_line(&self, predicate: impl Fn(&str) -> bool) -> Option<&str> { + let lines = self + .out + .rsplit(|b| *b == b'\n') + .flat_map(|l| l.rsplit(|b| *b == b'\r')); + for line in lines { + if let Ok(line) = std::str::from_utf8(line) { + if predicate(line) { + return Some(line); + } + } + } + None } /// Returns last non-empty line, if any. pub fn last_line(&self) -> &str { - self.rlines().find(|l| !l.is_empty()).unwrap_or_default() + self.rfind_line(|l| !l.is_empty()).unwrap_or_default() } } -#[test] -fn rlines_rn() { - let mut chunks = Chunks::default(); - chunks.push(b"something \r fooo \r\n"); - let mut rlines = chunks.rlines(); - assert_eq!(rlines.next(), Some(" fooo ")); - assert_eq!(rlines.next(), Some("something ")); -} - #[test] fn parse_ffmpeg_progress_chunk() { let out = "frame= 288 fps= 94 q=-0.0 size=N/A time=01:23:12.34 bitrate=N/A speed=3.94x \r"; diff --git a/src/vmaf.rs b/src/vmaf.rs index 5d1aa25..0f39e02 100644 --- a/src/vmaf.rs +++ b/src/vmaf.rs @@ -75,17 +75,129 @@ pub enum VmafOut { impl VmafOut { fn try_from_chunk(chunk: &[u8], chunks: &mut Chunks) -> Option { + const VMAF_SCORE_PRE: &str = "VMAF score: "; + chunks.push(chunk); - let line = chunks.last_line(); - if let Some(idx) = line.find("VMAF score: ") { + if let Some(line) = chunks.rfind_line(|l| l.contains(VMAF_SCORE_PRE)) { + let idx = line.find(VMAF_SCORE_PRE).unwrap(); return Some(Self::Done( - line[idx + "VMAF score: ".len()..].trim().parse().ok()?, + line[idx + VMAF_SCORE_PRE.len()..].trim().parse().ok()?, )); } - if let Some(progress) = FfmpegOut::try_parse(line) { + if let Some(progress) = FfmpegOut::try_parse(chunks.last_line()) { return Some(Self::Progress(progress)); } None } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn parse_vmaf_score_207() { + const FFMPEG_OUT: &str = r#"ffmpeg version n7.0.1 Copyright (c) 2000-2024 the FFmpeg developers + built with gcc 14.1.1 (GCC) 20240522 + configuration: --prefix=/usr --disable-debug --disable-static --disable-stripping --enable-amf --enable-avisynth --enable-cuda-llvm --enable-lto --enable-fontconfig --enable-frei0r --enable-gmp --enable-gpl --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libdav1d --enable-libdrm --enable-libdvdnav --enable-libdvdread --enable-libfreetype --enable-libfribidi --enable-libgsm --enable-libharfbuzz --enable-libiec61883 --enable-libjack --enable-libjxl --enable-libmodplug --enable-libmp3lame --enable-libopencore_amrnb --enable-libopencore_amrwb --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libplacebo --enable-libpulse --enable-librav1e --enable-librsvg --enable-librubberband --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libsvtav1 --enable-libtheora --enable-libv4l2 --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpl --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxcb --enable-libxml2 --enable-libxvid --enable-libzimg --enable-mbedtls --enable-nvdec --enable-nvenc --enable-opencl --enable-opengl --enable-shared --enable-vapoursynth --enable-version3 --enable-vulkan + libavutil 59. 8.100 / 59. 8.100 + libavcodec 61. 3.100 / 61. 3.100 + libavformat 61. 1.100 / 61. 1.100 + libavdevice 61. 1.100 / 61. 1.100 + libavfilter 10. 1.100 / 10. 1.100 + libswscale 8. 1.100 / 8. 1.100 + libswresample 5. 1.100 / 5. 1.100 + libpostproc 58. 1.100 / 58. 1.100 + + libavutil 59. 8.100 / 59. 8.100 + libavcodec 61. 3.100 / 61. 3.100 + libavformat 61. 1.100 / 61. 1.100 + libavdevice 61. 1.100 / 61. 1.100 + libavfilter 10. 1.100 / 10. 1.100 + libswscale 8. 1.100 / 8. 1.100 + libswresample 5. 1.100 / 5. 1.100 + libpostproc 58. 1.100 / 58. 1.100 +Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'C:\Users\Administrator\Personal_scripts\Python\PythonScripts\PythonScripts\src\.ab-av1-RM46M2PZOVjb\A11 崩三 黑曼巴之影_1.sample2+600f.av1.crf37.5.mp4': + Metadata: + major_brand : isom + minor_version : 512 + compatible_brands: isomav01iso2mp41 + title : Project 1 + date : 2019-07-11 + encoder : Lavf61.1.100 + Duration: 00:00:20.00, start: 0.000000, bitrate: 1562 kb/s + Stream #0:0[0x1](und): Video: av1 (libdav1d) (Main) (av01 / 0x31307661), yuv420p10le(tv, progressive), 1000x696, 1560 kb/s, SAR 1:1 DAR 125:87, 30 fps, 30 tbr, 15360 tbn (default) + Metadata: + handler_name : VideoHandler + vendor_id : [0][0][0][0] + encoder : Lavc61.3.100 libsvtav1 +Input #1, matroska,webm, from 'C:\Users\Administrator\Personal_scripts\Python\PythonScripts\PythonScripts\src\.ab-av1-RM46M2PZOVjb\A11 崩三 黑曼巴之影_1.sample2+600f.mkv': + Metadata: + title : Project 1 + DATE : 2019-07-11 + MAJOR_BRAND : isom + MINOR_VERSION : 512 + COMPATIBLE_BRANDS: isomiso2mp41 + ENCODER : Lavf61.1.100 + Duration: 00:00:20.00, start: 0.000000, bitrate: 6114 kb/s + Stream #1:0: Video: mpeg4 (Simple Profile), yuv420p, 1000x696 [SAR 1:1 DAR 125:87], 30 fps, 30 tbr, 1k tbn (default) + Metadata: + HANDLER_NAME : VideoHandler + VENDOR_ID : [0][0][0][0] + DURATION : 00:00:20.000000000 +Stream mapping: + Stream #0:0 (libdav1d) -> format:default + Stream #1:0 (mpeg4) -> format:default + libvmaf:default -> Stream #0:0 (wrapped_avframe) +Press [q] to stop, [?] for help +Output #0, null, to 'pipe:': + Metadata: + major_brand : isom + minor_version : 512 + compatible_brands: isomav01iso2mp41 + title : Project 1 + date : 2019-07-11 + encoder : Lavf61.1.100 + Stream #0:0: Video: wrapped_avframe, yuv420p10le(tv, progressive), 1552x1080 [SAR 5625:5626 DAR 125:87], q=2-31, 200 kb/s, 24 tbn + Metadata: + encoder : Lavc61.3.100 wrapped_avframe +frame= 48 fps=0.0 q=-0.0 size=N/A time=00:00:01.95 bitrate=N/A speed=3.79x +frame= 101 fps= 97 q=-0.0 size=N/A time=00:00:04.16 bitrate=N/A speed= 4x +frame= 156 fps=100 q=-0.0 size=N/A time=00:00:06.45 bitrate=N/A speed=4.14x +frame= 209 fps=101 q=-0.0 size=N/A time=00:00:08.66 bitrate=N/A speed= 4.2x +frame= 264 fps=102 q=-0.0 size=N/A time=00:00:10.95 bitrate=N/A speed=4.23x +frame= 319 fps=103 q=-0.0 size=N/A time=00:00:13.25 bitrate=N/A speed=4.26x +frame= 373 fps=103 q=-0.0 size=N/A time=00:00:15.50 bitrate=N/A speed=4.27x +frame= 429 fps=103 q=-0.0 size=N/A time=00:00:17.83 bitrate=N/A speed= 4.3x +frame= 482 fps=103 q=-0.0 size=N/A time=00:00:20.04 bitrate=N/A speed=4.29x +frame= 536 fps=104 q=-0.0 size=N/A time=00:00:22.29 bitrate=N/A speed=4.31x +frame= 589 fps=103 q=-0.0 size=N/A time=00:00:24.50 bitrate=N/A speed= 4.3x +[Parsed_libvmaf_6 @ 000002b296bac480] VMAF score: 94.826380 +[out#0/null @ 000002b2916f8b80] video:258KiB audio:0KiB subtitle:0KiB other streams:0KiB global headers:0KiB muxing overhead: unknown +frame= 600 fps=102 q=-0.0 Lsize=N/A time=00:00:24.95 bitrate=N/A speed=4.24x"#; + + const CHUNK_SIZE: usize = 64; + + let ffmpeg = FFMPEG_OUT.as_bytes(); + + let mut chunks = Chunks::default(); + let mut start_idx = 0; + let mut vmaf_score = None; + while start_idx < ffmpeg.len() { + let chunk = &ffmpeg[start_idx..(start_idx + CHUNK_SIZE).min(FFMPEG_OUT.len())]; + println!("* {}", String::from_utf8_lossy(chunk).trim()); + + if let Some(vmaf) = VmafOut::try_from_chunk(chunk, &mut chunks) { + println!("{vmaf:?}"); + if let VmafOut::Done(score) = vmaf { + vmaf_score = Some(score); + } + } + + start_idx += CHUNK_SIZE; + } + + assert_eq!(vmaf_score, Some(94.82638), "failed to parse vmaf score"); + } +}