Skip to main content

bambu_rs/server/
stream_record.rs

1//! Recording a camera's MJPEG stream to disk for the "plain" timelapse. For a
2//! camera that exposes a real `/stream`, this captures the actual continuous video
3//! instead of time-sampling `/snapshot`. The copy + reconnect logic is pure (the
4//! stream opener and the sink are injected), so it's unit-tested without a network:
5//! tests feed canned readers and a `Vec` sink, and drive the cancel signal.
6
7use std::io::{Read, Write};
8
9use super::camera::StreamOpen;
10
11/// How a single-connection copy ended.
12#[derive(Debug, PartialEq, Eq)]
13pub enum CopyEnd {
14    /// Upstream closed the body (EOF) — the caller reconnects if still recording.
15    Eof,
16    /// The recorder was asked to stop.
17    Cancelled,
18    /// The per-run byte cap was reached.
19    CapReached,
20    /// A read (upstream) or write (sink) error ended this connection — reconnect,
21    /// counting it as a failure. The bytes written *before* the error are still
22    /// reported so the caller's byte cap and totals stay correct.
23    Errored,
24}
25
26/// Aggregate outcome of a full recording run.
27#[derive(Debug, Default, PartialEq, Eq)]
28pub struct RecordStats {
29    /// Total bytes written to the sink across all connections.
30    pub bytes: u64,
31    /// Successful stream opens.
32    pub connections: u32,
33    /// Failed attempts (an open error, or a mid-stream read error).
34    pub failures: u32,
35}
36
37/// Copy from `reader` to `sink` until EOF, cancellation, the byte cap, or an
38/// error. `cancel` is checked between reads — a read already in flight finishes
39/// first, so this is cooperative (paired with a short read timeout on the real
40/// stream). Always returns the bytes written (even on error), so the caller's cap
41/// and totals stay correct across reconnects. Never writes past `cap_remaining`.
42pub fn copy_until(
43    reader: &mut dyn Read,
44    sink: &mut dyn Write,
45    cancel: &dyn Fn() -> bool,
46    cap_remaining: u64,
47    buf: &mut [u8],
48) -> (CopyEnd, u64) {
49    let mut written: u64 = 0;
50    loop {
51        if cancel() {
52            return (CopyEnd::Cancelled, written);
53        }
54        if written >= cap_remaining {
55            return (CopyEnd::CapReached, written);
56        }
57        let n = match reader.read(buf) {
58            Ok(0) => return (CopyEnd::Eof, written),
59            Ok(n) => n,
60            Err(_) => return (CopyEnd::Errored, written),
61        };
62        // Don't overshoot the cap — write only what fits, then stop.
63        let take = (n as u64).min(cap_remaining - written) as usize;
64        if sink.write_all(&buf[..take]).is_err() {
65            return (CopyEnd::Errored, written);
66        }
67        written += take as u64;
68        if take < n {
69            return (CopyEnd::CapReached, written);
70        }
71    }
72}
73
74/// Record `open`'s stream into `sink`: open → copy → on EOF/error reconnect (after
75/// `backoff`) until `cancel` fires or `max_bytes` is reached. `backoff(attempt)` is
76/// the inter-attempt pause (the real one sleeps; tests pass a no-op). The loop only
77/// ends on cancel/cap — a persistently-failing source keeps retrying because the
78/// recorder is meant to run for as long as the print is active, and the caller
79/// flips `cancel` when the print ends.
80pub fn record_loop(
81    open: &StreamOpen,
82    sink: &mut dyn Write,
83    cancel: &dyn Fn() -> bool,
84    max_bytes: u64,
85    buf_size: usize,
86    backoff: &dyn Fn(u32),
87) -> RecordStats {
88    let mut stats = RecordStats::default();
89    let mut buf = vec![0u8; buf_size.max(1)];
90    let mut attempts: u32 = 0;
91    loop {
92        if cancel() || stats.bytes >= max_bytes {
93            return stats;
94        }
95        if attempts > 0 {
96            backoff(attempts);
97            if cancel() {
98                return stats; // asked to stop during the backoff
99            }
100        }
101        attempts += 1;
102        match (open)() {
103            Ok(mut opened) => {
104                stats.connections += 1;
105                let (end, n) = copy_until(
106                    &mut *opened.reader,
107                    sink,
108                    cancel,
109                    max_bytes - stats.bytes,
110                    &mut buf,
111                );
112                stats.bytes += n; // always count what was written, even on error
113                match end {
114                    CopyEnd::Eof => {}                       // reconnect
115                    CopyEnd::Errored => stats.failures += 1, // count + reconnect
116                    CopyEnd::Cancelled | CopyEnd::CapReached => return stats,
117                }
118            }
119            Err(_) => stats.failures += 1, // open error → backoff + retry
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::server::camera::OpenedCameraStream;
128    use std::io::Cursor;
129    use std::sync::Arc;
130    use std::sync::atomic::{AtomicUsize, Ordering};
131
132    fn never() -> bool {
133        false
134    }
135
136    // ── copy_until ──
137
138    #[test]
139    fn copy_until_writes_everything_then_reports_eof() {
140        let mut r = Cursor::new(b"hello world".to_vec());
141        let mut sink: Vec<u8> = Vec::new();
142        let mut buf = [0u8; 4];
143        let (end, n) = copy_until(&mut r, &mut sink, &never, 1_000, &mut buf);
144        assert_eq!(end, CopyEnd::Eof);
145        assert_eq!(n, 11);
146        assert_eq!(sink, b"hello world");
147    }
148
149    #[test]
150    fn copy_until_stops_on_cancel_without_reading() {
151        let mut r = Cursor::new(b"data".to_vec());
152        let mut sink: Vec<u8> = Vec::new();
153        let mut buf = [0u8; 8];
154        let (end, n) = copy_until(&mut r, &mut sink, &|| true, 1_000, &mut buf);
155        assert_eq!(end, CopyEnd::Cancelled);
156        assert_eq!(n, 0);
157        assert!(sink.is_empty());
158    }
159
160    /// Yields `data`, then fails every subsequent read (a mid-stream drop).
161    struct BytesThenErr {
162        data: Vec<u8>,
163        pos: usize,
164    }
165    impl Read for BytesThenErr {
166        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
167            if self.pos >= self.data.len() {
168                return Err(std::io::Error::new(
169                    std::io::ErrorKind::ConnectionReset,
170                    "drop",
171                ));
172            }
173            let n = buf.len().min(self.data.len() - self.pos);
174            buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
175            self.pos += n;
176            Ok(n)
177        }
178    }
179
180    #[test]
181    fn copy_until_keeps_bytes_written_before_a_read_error() {
182        // The bug the cap relies on NOT having: a mid-stream error must still report
183        // the bytes already written, or a flaky stream blows past the cap.
184        let mut r = BytesThenErr {
185            data: b"abc".to_vec(),
186            pos: 0,
187        };
188        let mut sink: Vec<u8> = Vec::new();
189        let mut buf = [0u8; 2];
190        let (end, n) = copy_until(&mut r, &mut sink, &never, 1_000, &mut buf);
191        assert_eq!(end, CopyEnd::Errored);
192        assert_eq!(n, 3);
193        assert_eq!(sink, b"abc");
194    }
195
196    #[test]
197    fn copy_until_never_writes_past_the_cap() {
198        let mut r = Cursor::new(b"0123456789".to_vec());
199        let mut sink: Vec<u8> = Vec::new();
200        let mut buf = [0u8; 8];
201        let (end, n) = copy_until(&mut r, &mut sink, &never, 4, &mut buf);
202        assert_eq!(end, CopyEnd::CapReached);
203        assert_eq!(n, 4);
204        assert_eq!(sink, b"0123");
205    }
206
207    // ── record_loop ──
208
209    /// A `StreamOpen` that yields each entry in turn: `Some(bytes)` opens a reader
210    /// over those bytes (then EOF), `None` is an open failure. Out of entries ⇒
211    /// open failure. Also exposes the attempt counter for the cancel closure.
212    fn seq_opener(entries: Vec<Option<Vec<u8>>>) -> (StreamOpen, Arc<AtomicUsize>) {
213        let idx = Arc::new(AtomicUsize::new(0));
214        let counter = idx.clone();
215        let open: StreamOpen = Arc::new(move || {
216            let i = idx.fetch_add(1, Ordering::SeqCst);
217            match entries.get(i) {
218                Some(Some(data)) => Ok(OpenedCameraStream {
219                    content_type: "multipart/x-mixed-replace".to_string(),
220                    reader: Box::new(Cursor::new(data.clone())),
221                }),
222                _ => Err("no stream".to_string()),
223            }
224        });
225        (open, counter)
226    }
227
228    #[test]
229    fn record_loop_reconnects_after_each_eof() {
230        // Two readers, EOF each; a byte cap spanning both forces a reconnect
231        // between them — connections == 2 and the concatenated output prove the
232        // recorder resumed onto a fresh connection.
233        let (open, _) = seq_opener(vec![Some(b"aaa".to_vec()), Some(b"bbb".to_vec())]);
234        let mut sink: Vec<u8> = Vec::new();
235        let stats = record_loop(&open, &mut sink, &never, 6, 8, &|_| {});
236        assert_eq!(stats.connections, 2);
237        assert_eq!(stats.failures, 0);
238        assert_eq!(sink, b"aaabbb");
239    }
240
241    #[test]
242    fn record_loop_counts_a_failed_open_and_retries() {
243        // First open fails (failures == 1), the retry succeeds and is recorded.
244        let (open, _) = seq_opener(vec![None, Some(b"ok".to_vec())]);
245        let mut sink: Vec<u8> = Vec::new();
246        let stats = record_loop(&open, &mut sink, &never, 2, 8, &|_| {});
247        assert_eq!(stats.failures, 1);
248        assert_eq!(stats.connections, 1);
249        assert_eq!(sink, b"ok");
250    }
251
252    #[test]
253    fn record_loop_stops_immediately_when_cancelled_up_front() {
254        let (open, _idx) = seq_opener(vec![Some(b"x".to_vec())]);
255        let mut sink: Vec<u8> = Vec::new();
256        let stats = record_loop(&open, &mut sink, &|| true, 1_000, 8, &|_| {});
257        assert_eq!(stats.connections, 0);
258        assert_eq!(stats.bytes, 0);
259        assert!(sink.is_empty());
260    }
261
262    #[test]
263    fn record_loop_honours_the_byte_cap() {
264        // One long reader, cap below its length: copy stops at the cap and the run
265        // ends (CapReached), no reconnect.
266        let (open, _idx) = seq_opener(vec![Some(vec![7u8; 100])]);
267        let mut sink: Vec<u8> = Vec::new();
268        let stats = record_loop(&open, &mut sink, &never, 10, 8, &|_| {});
269        assert_eq!(stats.bytes, 10);
270        assert_eq!(stats.connections, 1);
271        assert_eq!(sink.len(), 10);
272    }
273}