heldar_kernel/services/
mediamtx.rs1use serde::Serialize;
5use serde_json::json;
6
7use crate::camera_url;
8use crate::error::{AppError, AppResult};
9use crate::models::Camera;
10use crate::state::AppState;
11
12#[derive(Debug, Serialize)]
13pub struct LiveUrls {
14 pub name: String,
15 pub hls_url: String,
16 pub webrtc_url: String,
17 pub rtsp_url: String,
18}
19
20pub async fn ensure_live(state: &AppState, camera_id: &str) -> AppResult<LiveUrls> {
22 let cam: Option<Camera> = sqlx::query_as::<_, Camera>("SELECT * FROM cameras WHERE id = ?")
23 .bind(camera_id)
24 .fetch_optional(&state.pool)
25 .await?;
26 let cam = cam.ok_or_else(|| AppError::NotFound(format!("camera {camera_id} not found")))?;
27
28 let source = camera_url::stream_url(&cam, "sub")
29 .or_else(|| camera_url::record_url(&cam))
30 .ok_or_else(|| AppError::BadRequest("camera has no stream URL".into()))?;
31
32 let name = format!("cam_{camera_id}");
33 let api = state.cfg.mediamtx_api_url.trim_end_matches('/');
34
35 let existing = state
36 .http
37 .get(format!("{api}/v3/config/paths/get/{name}"))
38 .send()
39 .await;
40 let already = matches!(existing, Ok(ref r) if r.status().is_success());
41
42 if !already {
43 let run_on_demand = format!(
49 "ffmpeg -nostdin -rtsp_transport tcp -timeout 10000000 -i {source} -an \
50-c:v libx264 -preset ultrafast -tune zerolatency -profile:v baseline -pix_fmt yuv420p -g 30 \
51-f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH"
52 );
53 let body = json!({
54 "runOnDemand": run_on_demand,
55 "runOnDemandRestart": true,
56 "runOnDemandCloseAfter": "10s",
57 });
58 let resp = state
59 .http
60 .post(format!("{api}/v3/config/paths/add/{name}"))
61 .json(&body)
62 .send()
63 .await
64 .map_err(|e| AppError::Other(anyhow::anyhow!("MediaMTX unreachable at {api}: {e}")))?;
65 let code = resp.status();
66 if !code.is_success() && code.as_u16() != 400 {
67 let txt = resp.text().await.unwrap_or_default();
68 return Err(AppError::Other(anyhow::anyhow!(
69 "MediaMTX add-path failed ({code}): {txt}"
70 )));
71 }
72 }
73
74 let hls = state.cfg.mediamtx_hls_base.trim_end_matches('/');
75 let webrtc = state.cfg.mediamtx_webrtc_base.trim_end_matches('/');
76 let rtsp = state.cfg.mediamtx_rtsp_base.trim_end_matches('/');
77 Ok(LiveUrls {
78 hls_url: format!("{hls}/{name}/index.m3u8"),
79 webrtc_url: format!("{webrtc}/{name}"),
80 rtsp_url: format!("{rtsp}/{name}"),
81 name,
82 })
83}