Skip to main content

ados_sdk/client/
media.rs

1//! The live-video and rig relays: `video.whep.offer`, `video.whep.close` and
2//! `sensors.rig.write`. The argument maps carry the field names of
3//! [`ados_protocol::media_relay`]'s request types, which the host decodes.
4
5use ados_protocol::media_relay::{
6    RigWriteReply, WhepOfferReply, RIG_CAPABILITY, RIG_WRITE_METHOD, WHEP_CAPABILITY,
7    WHEP_CLOSE_METHOD, WHEP_OFFER_METHOD,
8};
9
10use super::*;
11
12/// How long a relay call waits for the host. The host bounds each upstream
13/// exchange (connect plus answer) under this, so its own error arrives first.
14const RELAY_TIMEOUT: Duration = Duration::from_secs(12);
15
16/// The answer to a WHEP offer.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct WhepAnswer {
19    /// The SDP answer for the viewer.
20    pub sdp: String,
21    /// The opaque session location to pass to [`PluginIpcClient::whep_close`];
22    /// `None` when the media server named none.
23    pub location: Option<String>,
24}
25
26fn args(entries: &[(&str, &str)]) -> Value {
27    Value::Map(
28        entries
29            .iter()
30            .map(|(k, v)| (Value::from(*k), Value::from(*v)))
31            .collect(),
32    )
33}
34
35impl PluginIpcClient {
36    /// Open a WebRTC playback (WHEP) session on the configured camera
37    /// `camera_id` (its roster id) for a viewer's `offer_sdp`, and return the
38    /// answer. The host resolves the camera to the media leg publishing it.
39    /// Gated on `video.whep.relay` ([`ClientError::CapabilityDenied`] without
40    /// it). An id that is not a configured, enabled camera is
41    /// [`ClientError::Rpc`]`("unknown_camera: ..")`; the media server refusing
42    /// the offer or being down is [`ClientError::Rpc`]`("whep_unavailable: ..")`.
43    pub async fn whep_offer(
44        &self,
45        camera_id: &str,
46        offer_sdp: &str,
47    ) -> Result<WhepAnswer, ClientError> {
48        let args = args(&[("camera_id", camera_id), ("sdp", offer_sdp)]);
49        let reply = self
50            .send_request_within(WHEP_OFFER_METHOD, WHEP_CAPABILITY, args, RELAY_TIMEOUT)
51            .await?;
52        let reply: WhepOfferReply = decode_typed_reply(reply.args, WHEP_OFFER_METHOD)?;
53        Ok(WhepAnswer {
54            sdp: reply.sdp,
55            location: reply.location,
56        })
57    }
58
59    /// Close a session [`whep_offer`](Self::whep_offer) opened for this plugin,
60    /// by the location it returned. Any other location is refused as
61    /// [`ClientError::Rpc`]`("whep_unavailable: ..")`.
62    pub async fn whep_close(&self, location: &str) -> Result<(), ClientError> {
63        let args = args(&[("location", location)]);
64        let reply = self
65            .send_request_within(WHEP_CLOSE_METHOD, WHEP_CAPABILITY, args, RELAY_TIMEOUT)
66            .await?;
67        if let Some(error) = map_get_str(&reply.args, "error") {
68            return Err(ClientError::Rpc(format!("{error}: {WHEP_CLOSE_METHOD}")));
69        }
70        Ok(())
71    }
72
73    /// Store `rig` through the agent's `PUT /api/v1/sensors/rig` and return
74    /// the route's HTTP status and JSON body (`{"ok":true,"rig_id",
75    /// "calib_version"}`, or `{"error":"E_RIG_INVALID","detail"}` with 400).
76    /// Gated on `sensors.rig.write`. The route being unreachable is
77    /// [`ClientError::Rpc`]`("rig_unavailable: ..")`.
78    pub async fn put_rig(
79        &self,
80        rig: &serde_json::Value,
81    ) -> Result<(u16, serde_json::Value), ClientError> {
82        let args = args(&[("rig_json", rig.to_string().as_str())]);
83        let reply = self
84            .send_request_within(RIG_WRITE_METHOD, RIG_CAPABILITY, args, RELAY_TIMEOUT)
85            .await?;
86        let reply: RigWriteReply = decode_typed_reply(reply.args, RIG_WRITE_METHOD)?;
87        let body = serde_json::from_str(&reply.body_json)
88            .map_err(|e| ClientError::Rpc(format!("{RIG_WRITE_METHOD} answer is not JSON: {e}")))?;
89        Ok((reply.status, body))
90    }
91}