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;
8use serde_json::json;
9
10use crate::auth::{self, Principal};
11use crate::error::{AppError, AppResult};
12use crate::services::clip::ClipResult;
13use crate::services::{clip, snapshot};
14use crate::state::AppState;
15use crate::util;
16
17pub fn router() -> Router<AppState> {
18    Router::new()
19        .route("/api/v1/cameras/{id}/clip", post(export_clip))
20        .route("/api/v1/cameras/{id}/snapshot", get(snapshot_handler))
21}
22
23#[derive(Debug, Deserialize)]
24struct ClipRequest {
25    from: String,
26    to: String,
27}
28
29async fn export_clip(
30    State(st): State<AppState>,
31    principal: Principal,
32    Path(id): Path<String>,
33    Json(req): Json<ClipRequest>,
34) -> AppResult<Json<ClipResult>> {
35    // Operational action (viewer+); the extractor enforces auth when it is enabled.
36    principal.require(principal.can_view(), "export clips")?;
37    let from = util::parse_rfc3339(&req.from)
38        .ok_or_else(|| AppError::BadRequest("invalid `from` timestamp".into()))?;
39    let to = util::parse_rfc3339(&req.to)
40        .ok_or_else(|| AppError::BadRequest("invalid `to` timestamp".into()))?;
41    let result = clip::export_clip(&st, &id, from, to).await?;
42    auth::audit(
43        &st.pool,
44        &principal,
45        "export_clip",
46        "camera",
47        &id,
48        json!({ "from": from, "to": to }),
49    )
50    .await;
51    Ok(Json(result))
52}
53
54#[derive(Debug, Deserialize)]
55struct SnapshotQuery {
56    /// Recorded-frame timestamp (RFC3339). If omitted, a live frame is grabbed.
57    at: Option<String>,
58}
59
60async fn snapshot_handler(
61    State(st): State<AppState>,
62    principal: Principal,
63    Path(id): Path<String>,
64    Query(q): Query<SnapshotQuery>,
65) -> AppResult<Response> {
66    // Operational action (viewer+): a snapshot can contain faces/plates.
67    principal.require(principal.can_view(), "capture snapshots")?;
68    let bytes = match q.at {
69        Some(ref at) => {
70            let ts = util::parse_rfc3339(at)
71                .ok_or_else(|| AppError::BadRequest("invalid `at` timestamp".into()))?;
72            snapshot::snapshot_at(&st, &id, ts).await?
73        }
74        None => snapshot::snapshot_live(&st, &id).await?,
75    };
76
77    Response::builder()
78        .header(header::CONTENT_TYPE, "image/jpeg")
79        .header(header::CACHE_CONTROL, "no-store")
80        .body(Body::from(bytes))
81        .map_err(|e| AppError::Other(anyhow::anyhow!("building response: {e}")))
82}