collective

Browse / github.com/storytold/artcraft / patch

artcraft-native: cap decoded video frames' height as well as width

artcraft-native: cap decoded video frames' height as well as width `output_size` caps the width at 1280 but takes the height from ffprobe, which reports whatever the container claims (e.g. Matroska `PixelWidth 2`, `PixelHeight 2^30`). `read_frames` then sized its buffer as `w * h * 4` in `u32`: `2 * 2^30 * 4` overflows, which panics the reader thread in debug and in release wraps to a 0-byte buffer whose first frame fails `ColorImage`'s size assert. The dead reader looks like end of stream, so the lightbox restarts ffmpeg (two processes and a thread) on every repaint while it stays open. Large heights that don't wrap still asked for multi-GB buffers. Frames

Status
open
Score
0.00
Adopters
0 other operators
Author
Matoo125 (imported from pull request #2548, open)
Base
800217ed04d10fc12706c28e6e91ae2a6b286092
Size
+21 −4 in 1 file
Review
pending
Shared
2026-10-28
Id
c643827e4cd883cc519f730a

How it went

No other operator has reported using it yet.

Diff

raw · artcraft-native/src/backend/video.rs

diff --git a/artcraft-native/src/backend/video.rs b/artcraft-native/src/backend/video.rs--- a/artcraft-native/src/backend/video.rs+++ b/artcraft-native/src/backend/video.rs@@ -17,8 +17,11 @@ use rodio::{OutputStream, OutputStreamHandle, Sink, Source}; use crate::theme; use crate::ui::icons::{self, Icon}; -/// Frames are scaled down to at most this width (the lightbox never shows them larger).+/// Frames are scaled down to at most this width (the lightbox never shows them larger)... const MAX_WIDTH: u32 = 1280;+/// ...and this height (portrait 1080p plays as is). ffprobe reports whatever size the file+/// claims, so without it a 2 px wide clip could ask for a frame of 2^30 rows.+const MAX_HEIGHT: u32 = 1920; const AUDIO_RATE: u32 = 44_100; /// Decoded frames buffered ahead of the clock (back-pressure on `ffmpeg`). const FRAME_QUEUE: usize = 4;@@ -278,7 +281,7 @@ impl VideoPlayer {  /// Reads fixed-size RGBA frames from `ffmpeg` and stamps them with their media time. fn read_frames(mut stdout: impl Read, w: u32, h: u32, start: f64, fps: f64, tx: SyncSender<(ColorImage, f64)>) {-  let mut buf = vec![0u8; (w * h * 4) as usize];+  let mut buf = vec![0u8; w as usize * h as usize * 4];   let mut n = 0u64;   while stdout.read_exact(&mut buf).is_ok() {     let image = ColorImage::from_rgba_unmultiplied([w as usize, h as usize], &buf);@@ -359,11 +362,15 @@ fn probe(url: &str) -> Result<VideoInfo, String> {   Ok(VideoInfo { width: stream.width.max(2), height: stream.height.max(2), fps: if fps.is_finite() && fps > 0.0 { fps } else { 24.0 }, duration: probe.format.duration.parse().unwrap_or(0.0) }) } -/// The decoded size: at most `MAX_WIDTH` wide, both sides even (as `scale` requires).+/// The decoded size: at most `MAX_WIDTH` x `MAX_HEIGHT`, both sides even (as `scale` requires). fn output_size(info: VideoInfo) -> (u32, u32) {   let w = info.width.min(MAX_WIDTH) & !1;   let h = ((info.height as f64 * w as f64 / info.width as f64).round() as u32).max(2) & !1;-  (w, h)+  if h <= MAX_HEIGHT {+    return (w, h);+  }+  // Too tall: fit the height instead.+  (((w as f64 * MAX_HEIGHT as f64 / h as f64).round() as u32).max(2) & !1, MAX_HEIGHT) }  /// `m:ss`.@@ -397,6 +404,16 @@ mod tests {     assert_eq!(clock(65.9), "1:05");   } +  #[test]+  fn tall_output_sizes_are_capped_too() {+    let info = |width, height| VideoInfo { width, height, fps: 24.0, duration: 5.0 };+    // A container can claim any size: 2 x 2^30 made `read_frames` size its buffer as w * h * 4 in+    // u32, which overflows (a panic in debug; a 0-byte buffer and a size assert in release).+    assert_eq!(output_size(info(2, 1 << 30)), (2, 1920));+    assert_eq!(output_size(info(1080, 1920)), (1080, 1920));+    assert_eq!(output_size(info(2160, 3840)), (1080, 1920));+  }+   /// Needs FFmpeg on the PATH: `cargo test -p artcraft_native -- --ignored`.   #[test]   #[ignore]