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}