Skip to main content

heldar_kernel/routes/
playback_sessions.rs

1//! Segment-spanning HLS playback sessions.
2//!
3//! `POST /api/v1/cameras/{id}/playback/sessions` generates a trimmed HLS VOD playlist over a recorded
4//! time range so operators can scrub/seek through footage; `DELETE /api/v1/playback/sessions/{id}`
5//! tears it down (releasing the segment read-locks it held). Both are viewer+ (any authenticated
6//! principal); `from`/`to` are RFC3339 and evaluated as absolute UTC instants.
7
8use axum::extract::{Path, State};
9use axum::http::StatusCode;
10use axum::routing::post;
11use axum::{Json, Router};
12use serde::Deserialize;
13use serde_json::json;
14
15use crate::auth::{self, Principal};
16use crate::error::{AppError, AppResult};
17use crate::services::playback_session::{self, PlaybackSession};
18use crate::state::AppState;
19use crate::util;
20
21pub fn router() -> Router<AppState> {
22    Router::new()
23        .route(
24            "/api/v1/cameras/{id}/playback/sessions",
25            post(create_session),
26        )
27        .route(
28            "/api/v1/playback/sessions/{session_id}",
29            axum::routing::delete(delete_session),
30        )
31}
32
33#[derive(Debug, Deserialize)]
34struct CreateSessionRequest {
35    from: String,
36    to: String,
37}
38
39async fn create_session(
40    State(st): State<AppState>,
41    Path(id): Path<String>,
42    principal: Principal,
43    Json(req): Json<CreateSessionRequest>,
44) -> AppResult<Json<PlaybackSession>> {
45    // viewer+: any authenticated principal (the extractor enforces auth when it is enabled).
46    let from = util::parse_rfc3339(&req.from)
47        .ok_or_else(|| AppError::BadRequest("invalid `from` timestamp".into()))?;
48    let to = util::parse_rfc3339(&req.to)
49        .ok_or_else(|| AppError::BadRequest("invalid `to` timestamp".into()))?;
50    let session = playback_session::create_session(&st, &id, from, to).await?;
51    auth::audit(
52        &st.pool,
53        &principal,
54        "create_playback_session",
55        "camera",
56        &id,
57        json!({ "session_id": session.id, "from": from, "to": to }),
58    )
59    .await;
60    Ok(Json(session))
61}
62
63async fn delete_session(
64    State(st): State<AppState>,
65    Path(session_id): Path<String>,
66    principal: Principal,
67) -> AppResult<StatusCode> {
68    // viewer+: any authenticated principal.
69    playback_session::delete_session(&st, &session_id).await?;
70    auth::audit(
71        &st.pool,
72        &principal,
73        "delete_playback_session",
74        "playback_session",
75        &session_id,
76        json!({}),
77    )
78    .await;
79    Ok(StatusCode::NO_CONTENT)
80}