use crate::AppState;
use axum::body::Body;
use axum::extract::{Path, State};
use axum::http::{header, Response, StatusCode};
use bytes::Bytes;
use std::time::Duration;
const BOUNDARY: &str = "frame";
const FIRST_FRAME_WAIT: Duration = Duration::from_secs(5);
pub async fn snapshot_handler(
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<Response<Body>, StatusCode> {
let camera = state.get(&id).ok_or(StatusCode::NOT_FOUND)?;
let mut lane = camera.jpeg_lane().await;
let jpeg = match lane.latest() {
Some(jpeg) => jpeg,
None => tokio::time::timeout(FIRST_FRAME_WAIT, lane.wait_next())
.await
.ok()
.flatten()
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?,
};
Response::builder()
.header(header::CONTENT_TYPE, "image/jpeg")
.header(header::CACHE_CONTROL, "no-cache, no-store, must-revalidate")
.body(Body::from(jpeg))
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
}
pub async fn mjpeg_handler(
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<Response<Body>, StatusCode> {
let camera = state.get(&id).ok_or(StatusCode::NOT_FOUND)?;
let lane = camera.jpeg_lane().await;
let stream = futures::stream::unfold(lane, |mut lane| async move {
let jpeg = lane.wait_next().await?;
Some((Ok::<Bytes, std::convert::Infallible>(multipart_part(&jpeg)), lane))
});
Response::builder()
.header(header::CONTENT_TYPE, format!("multipart/x-mixed-replace; boundary={BOUNDARY}"))
.header(header::CACHE_CONTROL, "no-cache, no-store, must-revalidate")
.body(Body::from_stream(stream))
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
}
fn multipart_part(jpeg: &[u8]) -> Bytes {
let mut part = Vec::with_capacity(jpeg.len() + 96);
part.extend_from_slice(format!("--{BOUNDARY}\r\n").as_bytes());
part.extend_from_slice(b"Content-Type: image/jpeg\r\nContent-Length: ");
part.extend_from_slice(jpeg.len().to_string().as_bytes());
part.extend_from_slice(b"\r\n\r\n");
part.extend_from_slice(jpeg);
part.extend_from_slice(b"\r\n");
Bytes::from(part)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_part_carries_the_boundary_type_and_exact_length() {
let jpeg = [0xff, 0xd8, 0x00, 0x01, 0xff, 0xd9];
let part = multipart_part(&jpeg);
let text = String::from_utf8_lossy(&part);
assert!(text.starts_with("--frame\r\n"), "{text:?}");
assert!(text.contains("Content-Type: image/jpeg"), "{text:?}");
assert!(text.contains("Content-Length: 6"), "{text:?}");
assert!(part.ends_with(b"\r\n"));
}
#[test]
fn the_payload_survives_framing_byte_for_byte() {
let jpeg = [0xff, 0xd8, 0x0a, 0x0d, 0x2d, 0x2d, 0xff, 0xd9];
let part = multipart_part(&jpeg);
let header_end = part
.windows(4)
.position(|w| w == b"\r\n\r\n")
.expect("headers are terminated")
+ 4;
assert_eq!(&part[header_end..part.len() - 2], &jpeg, "bytes that look like framing must survive");
}
#[test]
fn an_empty_payload_still_produces_a_valid_part() {
let text = String::from_utf8_lossy(&multipart_part(&[])).to_string();
assert!(text.contains("Content-Length: 0"), "{text:?}");
}
}