1use 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
18static STOP: AtomicBool = AtomicBool::new(false);
20
21const READ_TIMEOUT: Duration = Duration::from_millis(250);
23
24pub 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
61pub 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 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
103fn 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
140const POLL: Duration = Duration::from_millis(100);
142
143pub 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
182fn 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
192fn 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
214fn 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
235struct SocketFile {
237 listener: UnixListener,
238 path: PathBuf,
239}
240
241impl SocketFile {
242 fn bind(path: &Path) -> Result<Self> {
243 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)] fn 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 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 #[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}