Skip to main content

ops/
ingest.rs

1//! Frames from other processes: raw rgb24 on stdin (`show stream`) or over a
2//! unix socket with a header per client (`show serve`). Both send through the
3//! same `Wall`/`Pacer` path as `show video`.
4
5use crate::display::{load_canvas, wall_settings};
6use crate::util::warn;
7use crate::{Ctx, Progress};
8use anyhow::{Context, Result};
9use wall::{Canvas, Frame};
10use sources::raw::{Header, RawSource};
11use sources::{Fit, FrameSource};
12use std::io;
13use std::os::unix::net::{UnixListener, UnixStream};
14use std::path::{Path, PathBuf};
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::time::Duration;
17
18/// Set by SIGINT/SIGTERM; `show serve` checks it between reads.
19static STOP: AtomicBool = AtomicBool::new(false);
20
21/// How long a socket read blocks before `show serve` looks at `STOP` again.
22const READ_TIMEOUT: Duration = Duration::from_millis(250);
23
24/// Show raw rgb24 frames read from stdin, `size` pixels each (the canvas
25/// size when `None`), at `fps`.
26pub fn stream(
27    ctx: &Ctx,
28    size: Option<(u16, u16)>,
29    fps: u32,
30    fit: &str,
31    layout: Option<&str>,
32    p: &mut dyn Progress,
33) -> Result<()> {
34    let canvas = load_canvas(ctx, layout)?;
35    let fit: Fit = fit.parse()?;
36    let (w, h) = size.map_or((canvas.width, canvas.height), |(w, h)| {
37        (u32::from(w), u32::from(h))
38    });
39    anyhow::ensure!(w > 0 && h > 0, "size must be at least 1x1");
40
41    let mut source = RawSource::new(io::stdin().lock(), w, h);
42    let mut wall = driver::Wall::open(&ctx.iface, canvas.clone(), wall_settings(ctx))?;
43    let mut pacer = driver::Pacer::new(fps);
44    let mut src = Frame::black(w, h);
45    let mut out = Frame::black(canvas.width, canvas.height);
46    while source
47        .next_frame(&mut src)
48        .context("read frame from stdin")?
49    {
50        wall.show(fitted(&src, fit, &canvas, &mut out))?;
51        pacer.wait();
52    }
53    p.out(&format!(
54        "{} frames, {:.1} fps",
55        wall.frames_sent(),
56        pacer.achieved_fps()
57    ));
58    Ok(())
59}
60
61/// Serve one client at a time on a unix socket at `path`.
62///
63/// Each client sends a [`Header`] then frames, paced at the header's fps. The
64/// panel keeps the last frame between clients. Ctrl-C removes the socket and
65/// exits.
66pub fn serve(
67    ctx: &Ctx,
68    path: &str,
69    fit: &str,
70    layout: Option<&str>,
71    p: &mut dyn Progress,
72) -> Result<()> {
73    let canvas = load_canvas(ctx, layout)?;
74    let fit: Fit = fit.parse()?;
75    let socket = SocketFile::bind(Path::new(path))?;
76    socket.listener.set_nonblocking(true)?;
77    install_stop_handler();
78
79    let mut wall = driver::Wall::open(&ctx.iface, canvas.clone(), wall_settings(ctx))?;
80    let mut out = Frame::black(canvas.width, canvas.height);
81    p.err(&format!("listening on {path}"));
82    while !STOP.load(Ordering::Relaxed) {
83        let stream = match socket.listener.accept() {
84            Ok((stream, _)) => stream,
85            Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
86                std::thread::sleep(Duration::from_millis(50));
87                continue;
88            }
89            Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
90            Err(e) => return Err(e).context("accept"),
91        };
92        // BSD accept() hands out the listener's non-blocking flag.
93        stream.set_nonblocking(false)?;
94        stream.set_read_timeout(Some(READ_TIMEOUT))?;
95        if let Err(e) = serve_client(stream, &mut wall, &canvas, fit, &mut out, p) {
96            warn(p, format!("client: {e:#}"));
97        }
98    }
99    drop(socket);
100    Ok(())
101}
102
103/// Show one client's frames until it disconnects or `STOP` is set.
104fn serve_client(
105    mut stream: UnixStream,
106    wall: &mut driver::Wall,
107    canvas: &Canvas,
108    fit: Fit,
109    out: &mut Frame,
110    p: &mut dyn Progress,
111) -> Result<()> {
112    let header = Header::read(&mut stream).context("read stream header")?;
113    p.err(&format!(
114        "client: {}x{} at {} fps",
115        header.width, header.height, header.fps
116    ));
117    let (w, h) = (u32::from(header.width), u32::from(header.height));
118    let mut source = RawSource::new(stream, w, h);
119    let mut pacer = driver::Pacer::new(u32::from(header.fps));
120    let mut src = Frame::black(w, h);
121    while !STOP.load(Ordering::Relaxed) {
122        match source.read_frame(&mut src) {
123            Ok(true) => {
124                wall.show(fitted(&src, fit, canvas, out))?;
125                pacer.wait();
126            }
127            Ok(false) => break,
128            Err(e)
129                if matches!(
130                    e.kind(),
131                    io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
132                ) => {}
133            Err(e) => return Err(e).context("read frame"),
134        }
135    }
136    p.err(&format!("client: gone after {} frames", wall.frames_sent()));
137    Ok(())
138}
139
140/// How long a channel read blocks before the loop looks at the cancel flag.
141const POLL: Duration = Duration::from_millis(100);
142
143/// Show frames handed over by another thread: the daemon's `POST
144/// /show/frame` pushes one `Frame` per request into `rx`, this holds the
145/// `Wall` and draws them.
146///
147/// Ends when the sender is dropped, when `idle` passes with no frame, or
148/// when the job is cancelled. Reports the frame count as its last line.
149///
150/// # Errors
151/// Fails if the link cannot be opened or a frame cannot be sent.
152pub fn stream_channel(
153    ctx: &Ctx,
154    canvas: Canvas,
155    rx: &std::sync::mpsc::Receiver<Frame>,
156    fit: Fit,
157    idle: Duration,
158    p: &mut dyn Progress,
159) -> Result<()> {
160    use std::sync::mpsc::RecvTimeoutError;
161    let mut wall = driver::Wall::open(&ctx.iface, canvas.clone(), wall_settings(ctx))?;
162    let mut out = Frame::black(canvas.width, canvas.height);
163    let mut last = std::time::Instant::now();
164    while !p.cancelled() {
165        match rx.recv_timeout(POLL) {
166            Ok(src) => {
167                wall.show(fitted(&src, fit, &canvas, &mut out))?;
168                last = std::time::Instant::now();
169            }
170            Err(RecvTimeoutError::Timeout) if last.elapsed() < idle => {}
171            Err(RecvTimeoutError::Timeout) => {
172                p.err(&format!("no frame for {} s", idle.as_secs()));
173                break;
174            }
175            Err(RecvTimeoutError::Disconnected) => break,
176        }
177    }
178    p.out(&format!("{} frames", wall.frames_sent()));
179    Ok(())
180}
181
182/// `src` itself when it is already canvas-sized, else `src` fitted into `out`.
183fn fitted<'a>(src: &'a Frame, fit: Fit, canvas: &Canvas, out: &'a mut Frame) -> &'a Frame {
184    if (src.width, src.height) == (canvas.width, canvas.height) {
185        src
186    } else {
187        fit_into(src, fit, out);
188        out
189    }
190}
191
192/// Scaled size and placement of a `sw` x `sh` image fitted to `w` x `h`:
193/// `(scaled_w, scaled_h, dst_x, dst_y, src_x, src_y)`.
194fn fit_geometry(fit: Fit, sw: u32, sh: u32, w: u32, h: u32) -> (u32, u32, u32, u32, u32, u32) {
195    let (sw, sh, w, h) = (f64::from(sw), f64::from(sh), f64::from(w), f64::from(h));
196    let scale = match fit {
197        Fit::Stretch => return (w as u32, h as u32, 0, 0, 0, 0),
198        Fit::Contain => (w / sw).min(h / sh),
199        Fit::Cover => (w / sw).max(h / sh),
200    };
201    let rw = (sw * scale).round().max(1.0);
202    let rh = (sh * scale).round().max(1.0);
203    let pad = |space: f64, size: f64| ((space - size) / 2.0).round().max(0.0) as u32;
204    (
205        rw as u32,
206        rh as u32,
207        pad(w, rw),
208        pad(h, rh),
209        pad(rw, w),
210        pad(rh, h),
211    )
212}
213
214/// Resample `src` into `out` (already the canvas size) honouring `fit`.
215/// One allocation per frame for the scaled image; the same-size path in
216/// [`fitted`] has none.
217fn fit_into(src: &Frame, fit: Fit, out: &mut Frame) {
218    use image::{imageops, ImageBuffer, Rgb};
219    let (w, h) = (out.width, out.height);
220    let (rw, rh, dx, dy, sx, sy) = fit_geometry(fit, src.width, src.height, w, h);
221    let Some(img) = ImageBuffer::<Rgb<u8>, &[u8]>::from_raw(src.width, src.height, src.as_bytes())
222    else {
223        return;
224    };
225    let scaled = imageops::resize(&img, rw, rh, imageops::FilterType::Triangle);
226    out.as_bytes_mut().fill(0);
227    let cols = w.saturating_sub(dx).min(rw.saturating_sub(sx)) as usize;
228    let rows = h.saturating_sub(dy).min(rh.saturating_sub(sy));
229    for y in 0..rows {
230        let from = &scaled.as_raw()[((sy + y) * rw + sx) as usize * 3..][..cols * 3];
231        out.as_bytes_mut()[((dy + y) * w + dx) as usize * 3..][..cols * 3].copy_from_slice(from);
232    }
233}
234
235/// A bound listener whose socket file is removed when dropped.
236struct SocketFile {
237    listener: UnixListener,
238    path: PathBuf,
239}
240
241impl SocketFile {
242    fn bind(path: &Path) -> Result<Self> {
243        // A stale file from an earlier run would make bind fail.
244        if let Err(e) = std::fs::remove_file(path) {
245            if e.kind() != io::ErrorKind::NotFound {
246                return Err(e).with_context(|| format!("remove {}", path.display()));
247            }
248        }
249        let listener =
250            UnixListener::bind(path).with_context(|| format!("bind {}", path.display()))?;
251        Ok(Self {
252            listener,
253            path: path.to_path_buf(),
254        })
255    }
256}
257
258impl Drop for SocketFile {
259    fn drop(&mut self) {
260        let _ = std::fs::remove_file(&self.path);
261    }
262}
263
264extern "C" fn on_stop(_sig: libc::c_int) {
265    STOP.store(true, Ordering::Relaxed);
266}
267
268#[allow(unsafe_code)] // registers a handler that only stores a flag
269fn install_stop_handler() {
270    unsafe {
271        libc::signal(
272            libc::SIGINT,
273            on_stop as extern "C" fn(libc::c_int) as libc::sighandler_t,
274        );
275        libc::signal(
276            libc::SIGTERM,
277            on_stop as extern "C" fn(libc::c_int) as libc::sighandler_t,
278        );
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use sources::{pattern, Pattern};
286
287    #[test]
288    fn same_size_frames_pass_through_untouched() {
289        let canvas = Canvas::single(8, 4);
290        let src = pattern(Pattern::Gradient, 8, 4);
291        let mut out = Frame::black(8, 4);
292        let shown = fitted(&src, Fit::Contain, &canvas, &mut out);
293        assert!(std::ptr::eq(
294            std::ptr::from_ref(shown),
295            std::ptr::from_ref(&src)
296        ));
297    }
298
299    #[test]
300    fn fit_geometry_pads_or_crops_to_keep_the_aspect() {
301        // 2:1 source onto a 4:1 wall.
302        assert_eq!(
303            fit_geometry(Fit::Stretch, 64, 32, 128, 32),
304            (128, 32, 0, 0, 0, 0)
305        );
306        assert_eq!(
307            fit_geometry(Fit::Contain, 64, 32, 128, 32),
308            (64, 32, 32, 0, 0, 0)
309        );
310        assert_eq!(
311            fit_geometry(Fit::Cover, 64, 32, 128, 32),
312            (128, 64, 0, 0, 0, 16)
313        );
314    }
315
316    #[test]
317    fn contain_letterboxes_a_white_source_with_black_bars() {
318        let canvas = Canvas::single(8, 4);
319        let src = pattern(Pattern::White, 4, 4);
320        let mut out = Frame::black(8, 4);
321        let f = fitted(&src, Fit::Contain, &canvas, &mut out);
322        assert_eq!(f.pixel(0, 0), [0, 0, 0]);
323        assert_eq!(f.pixel(2, 0), [255, 255, 255]);
324        assert_eq!(f.pixel(5, 3), [255, 255, 255]);
325        assert_eq!(f.pixel(7, 3), [0, 0, 0]);
326    }
327
328    /// Microseconds to resample a 1920x1080 source onto a fifty-card
329    /// 1280x320 wall. Run with
330    /// `cargo test --release -p ops -- --ignored --nocapture`.
331    #[test]
332    #[ignore = "timing; run in release with --nocapture"]
333    fn fit_into_time_for_fifty_cards() {
334        const FRAMES: u32 = 100;
335        let src = pattern(Pattern::Gradient, 1920, 1080);
336        let mut out = Frame::black(1280, 320);
337        for fit in [Fit::Contain, Fit::Cover, Fit::Stretch] {
338            let t = std::time::Instant::now();
339            for _ in 0..FRAMES {
340                fit_into(&src, fit, &mut out);
341            }
342            let us = t.elapsed().as_secs_f64() * 1e6 / f64::from(FRAMES);
343            println!("fit_into {fit:?} 1920x1080 -> 1280x320: {us:.0} us/frame");
344        }
345        std::hint::black_box(&out);
346    }
347
348    #[test]
349    fn cover_and_stretch_fill_the_whole_wall() {
350        let canvas = Canvas::single(8, 4);
351        let src = pattern(Pattern::White, 4, 4);
352        let mut out = Frame::black(8, 4);
353        for fit in [Fit::Cover, Fit::Stretch] {
354            let f = fitted(&src, fit, &canvas, &mut out);
355            assert!(f.as_bytes().iter().all(|&b| b == 255), "{fit:?}");
356        }
357    }
358}