Skip to main content

heldar_kernel/routes/
playback.rs

1use axum::body::Body;
2use axum::extract::{Path, Query, State};
3use axum::http::header;
4use axum::response::Response;
5use axum::routing::{get, post};
6use axum::{Json, Router};
7use serde::Deserialize;
8
9use crate::error::{AppError, AppResult};
10use crate::services::clip::ClipResult;
11use crate::services::{clip, snapshot};
12use crate::state::AppState;
13use crate::util;
14
15pub fn router() -> Router<AppState> {
16    Router::new()
17        .route("/api/v1/cameras/{id}/clip", post(export_clip))
18        .route("/api/v1/cameras/{id}/snapshot", get(snapshot_handler))
19}
20
21#[derive(Debug, Deserialize)]
22struct ClipRequest {
23    from: String,
24    to: String,
25}
26
27async fn export_clip(
28    State(st): State<AppState>,
29    Path(id): Path<String>,
30    Json(req): Json<ClipRequest>,
31) -> AppResult<Json<ClipResult>> {
32    let from = util::parse_rfc3339(&req.from)
33        .ok_or_else(|| AppError::BadRequest("invalid `from` timestamp".into()))?;
34    let to = util::parse_rfc3339(&req.to)
35        .ok_or_else(|| AppError::BadRequest("invalid `to` timestamp".into()))?;
36    Ok(Json(clip::export_clip(&st, &id, from, to).await?))
37}
38
39#[derive(Debug, Deserialize)]
40struct SnapshotQuery {
41    /// Recorded-frame timestamp (RFC3339). If omitted, a live frame is grabbed.
42    at: Option<String>,
43}
44
45async fn snapshot_handler(
46    State(st): State<AppState>,
47    Path(id): Path<String>,
48    Query(q): Query<SnapshotQuery>,
49) -> AppResult<Response> {
50    let bytes = match q.at {
51        Some(ref at) => {
52            let ts = util::parse_rfc3339(at)
53                .ok_or_else(|| AppError::BadRequest("invalid `at` timestamp".into()))?;
54            snapshot::snapshot_at(&st, &id, ts).await?
55        }
56        None => snapshot::snapshot_live(&st, &id).await?,
57    };
58
59    Response::builder()
60        .header(header::CONTENT_TYPE, "image/jpeg")
61        .header(header::CACHE_CONTROL, "no-store")
62        .body(Body::from(bytes))
63        .map_err(|e| AppError::Other(anyhow::anyhow!("building response: {e}")))
64}