dahua-camera-server 0.3.1

axum HTTP, SSE and WebSocket API for the dahua-camera stack
Documentation
//! Route table.

use crate::handlers;
use dahua_camera_rtsp::CameraService;
use axum::routing::{get, post};
use axum::Router;
use std::path::Path;
use std::sync::Arc;
use tower_http::cors::CorsLayer;
use tower_http::services::{ServeDir, ServeFile};

/// Default asset directory, relative to the working directory.
const DEFAULT_ASSETS: &str = "static";

/// Build the HTTP application, serving assets from `./static`.
///
/// Equivalent to [`router_with_assets`] with the default directory. Use that
/// instead when embedding the service in a binary whose working directory is
/// not the asset directory — which is every consumer that is not this crate's
/// own binary.
///
/// # Security
/// [`CorsLayer::permissive`] is deliberate but only safe under the deployment
/// this crate assumes: the listener binds to loopback and the Tauri shell is
/// the only client. Binding to a routable address without also tightening this
/// exposes every camera on the vehicle LAN to any page the operator visits.
pub fn router(service: Arc<CameraService>) -> Router {
    router_with_assets(service, DEFAULT_ASSETS)
}

/// Build the HTTP application, serving static assets from `assets`.
///
/// Unmatched paths fall through to `{assets}/index.html`, so a single-page UI
/// can own its own client-side routing. A missing directory is not an error:
/// the API still works and asset requests 404, which is the right behaviour
/// for a headless deployment that has no UI at all.
pub fn router_with_assets(service: Arc<CameraService>, assets: impl AsRef<Path>) -> Router {
    let state = crate::AppStateInner::new(service);
    let assets = assets.as_ref();
    let statics = ServeDir::new(assets).fallback(ServeFile::new(assets.join("index.html")));

    let api = Router::new()
        .route("/api/cameras", get(handlers::status::list_cameras))
        .route("/api/cameras/{id}", get(handlers::status::get_camera))
        .route("/api/cameras/{id}/control", post(handlers::control::control_camera))
        .route("/api/cameras/{id}/cgi_snapshot", get(handlers::control::cgi_snapshot))
        .route("/stream/{id}", get(handlers::stream_ws::stream_handler))
        .route("/ws/events", get(handlers::events_ws::events_handler))
        // Events go over their OWN stream, never multiplexed onto the video
        // socket: a chatty event feed must not be able to delay a frame.
        .route("/api/events", get(handlers::events_sse::events_sse))
        .route("/api/cameras/{id}/capabilities", get(handlers::capabilities::get_capabilities));

    #[cfg(feature = "alarms")]
    let api = api
        .route("/api/alarms", get(handlers::alarms::list_alarms))
        .route("/api/alarms/ack", post(handlers::alarms::acknowledge_all))
        .route("/api/alarms/{id}/ack", post(handlers::alarms::acknowledge));

    #[cfg(feature = "decode")]
    let api = api
        .route("/mjpeg/{id}", get(handlers::mjpeg::mjpeg_handler))
        .route("/snapshot/{id}", get(handlers::mjpeg::snapshot_handler));

    api.layer(CorsLayer::permissive()).fallback_service(statics).with_state(state)
}