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    principal.require(principal.can_view(), "create playback sessions")?;
47    let from = util::parse_rfc3339(&req.from)
48        .ok_or_else(|| AppError::BadRequest("invalid `from` timestamp".into()))?;
49    let to = util::parse_rfc3339(&req.to)
50        .ok_or_else(|| AppError::BadRequest("invalid `to` timestamp".into()))?;
51    let session = playback_session::create_session(&st, &id, from, to).await?;
52    auth::audit(
53        &st.pool,
54        &principal,
55        "create_playback_session",
56        "camera",
57        &id,
58        json!({ "session_id": session.id, "from": from, "to": to }),
59    )
60    .await;
61    Ok(Json(session))
62}
63
64async fn delete_session(
65    State(st): State<AppState>,
66    Path(session_id): Path<String>,
67    principal: Principal,
68) -> AppResult<StatusCode> {
69    // viewer+: any authenticated principal.
70    principal.require(principal.can_view(), "delete playback sessions")?;
71    playback_session::delete_session(&st, &session_id).await?;
72    auth::audit(
73        &st.pool,
74        &principal,
75        "delete_playback_session",
76        "playback_session",
77        &session_id,
78        json!({}),
79    )
80    .await;
81    Ok(StatusCode::NO_CONTENT)
82}