Skip to main content

videosdk/resources/
hls.rs

1//! The HLS API.
2
3use std::sync::Arc;
4
5use futures_util::Stream;
6use reqwest::Method;
7use serde::{Deserialize, Serialize};
8use serde_json::{Map, Value};
9
10use crate::client::{CallOptions, Client};
11use crate::common::{
12    CompositionConfig, LayoutPriority, ResourceLinks, TranscriptionConfig, WebhookDeliverySummary,
13};
14use crate::error::{Error, Result};
15use crate::pagination::{auto_page, paginate, ListParams, Page, PageFetcher};
16use crate::query::QueryBuilder;
17use crate::resources::egress::{
18    composition_to_config, to_egress_handle, Composition, EgressHandle, EgressType, StopTarget,
19    StopWire,
20};
21use crate::resources::escape;
22
23const PATH: &str = "/v2/hls";
24
25/// The image format of a thumbnail capture.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum HlsCaptureFormat {
29    /// PNG.
30    Png,
31    /// JPEG.
32    Jpg,
33    /// WebP.
34    Webp,
35}
36
37/// An HLS stream, or a finished HLS recording.
38#[derive(Debug, Clone, Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct HlsStream {
41    /// The stream id.
42    pub id: String,
43    /// The composer that produced the stream.
44    pub composer_id: Option<String>,
45    /// The room being streamed.
46    pub room_id: Option<String>,
47    /// The session being streamed.
48    pub session_id: Option<String>,
49    /// The stream's mode.
50    pub mode: Option<String>,
51    /// When the stream started.
52    pub start: Option<String>,
53    /// When the stream ended, or `None` if it is still running.
54    pub end: Option<String>,
55    /// The stream's quality tier.
56    pub quality: Option<String>,
57    /// The stream's orientation.
58    pub orientation: Option<String>,
59    /// The thumbnail captures taken from this stream.
60    #[serde(default, deserialize_with = "crate::common::null_to_default")]
61    pub capture_log: Vec<Value>,
62    /// The stream's encryption settings.
63    pub encryption: Option<Map<String, Value>>,
64    /// The stream's duration, in seconds.
65    pub duration: Option<f64>,
66    /// A summary of the webhook deliveries for this stream.
67    pub webhook: Option<WebhookDeliverySummary>,
68    /// HATEOAS-style links to related resources.
69    #[serde(default, deserialize_with = "crate::common::null_to_default")]
70    pub links: ResourceLinks,
71    /// The downstream URL.
72    pub downstream_url: Option<String>,
73    /// The HLS playback URL.
74    pub playback_hls_url: Option<String>,
75    /// The livestream URL.
76    pub livestream_url: Option<String>,
77    /// Any fields the server returned that this SDK does not model yet.
78    #[serde(flatten)]
79    pub extra: Map<String, Value>,
80}
81
82/// The parameters for [`HlsResource::start`].
83#[derive(Debug, Clone, Default)]
84pub struct HlsStartParams {
85    /// The layout, quality, orientation and theme.
86    pub composition: Option<Composition>,
87    /// Transcribes and optionally summarizes the stream.
88    pub transcription: Option<TranscriptionConfig>,
89    /// A webhook to notify about the stream.
90    pub webhook_url: Option<String>,
91    /// A recording configuration to start alongside HLS.
92    pub recording: Option<Value>,
93    /// An advanced custom template URL.
94    ///
95    /// Prefer [`CompositionLayout::Custom`](crate::CompositionLayout::Custom),
96    /// which overrides this when both are set.
97    pub template_url: Option<String>,
98    /// A pre-acquired resource-pool unit id.
99    pub resource_id: Option<String>,
100}
101
102#[derive(Debug, Serialize)]
103#[serde(rename_all = "camelCase")]
104struct HlsStartWire<'a> {
105    room_id: &'a str,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    config: Option<CompositionConfig>,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    template_url: Option<String>,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    transcription: Option<&'a TranscriptionConfig>,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    webhook_url: Option<&'a str>,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    recording: Option<&'a Value>,
116    #[serde(skip_serializing_if = "Option::is_none")]
117    resource_id: Option<&'a str>,
118}
119
120/// The parameters for [`HlsResource::capture`].
121#[derive(Debug, Clone, Default, Serialize)]
122#[serde(rename_all = "camelCase")]
123pub struct HlsCaptureParams {
124    /// The room to capture from.
125    pub room_id: String,
126    /// The image format.
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub format: Option<HlsCaptureFormat>,
129    /// The capture width, in pixels.
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub width: Option<u32>,
132    /// The capture height, in pixels.
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub height: Option<u32>,
135    /// Captures the frame at this timestamp, in seconds. Omit to capture live.
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub time: Option<u32>,
138}
139
140/// The result of [`HlsResource::capture`].
141#[derive(Debug, Clone, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct HlsCaptureResult {
144    /// The server's confirmation message.
145    pub message: String,
146    /// The room the frame was captured from.
147    pub room_id: String,
148    /// Where the captured image was stored.
149    pub file_path: Option<String>,
150    /// The captured image's size, in bytes.
151    pub file_size: Option<i64>,
152    /// The captured image's file name.
153    pub file_name: Option<String>,
154    /// Additional metadata the server returned about the capture.
155    pub meta: Option<Value>,
156    /// Any fields the server returned that this SDK does not model yet.
157    #[serde(flatten)]
158    pub extra: Map<String, Value>,
159}
160
161/// The query parameters for [`HlsResource::list`].
162#[derive(Debug, Clone, Default)]
163pub struct HlsListParams {
164    /// The 1-based page number.
165    pub page: Option<u32>,
166    /// Items per page.
167    pub per_page: Option<u32>,
168    /// An opaque cursor from a previous page.
169    pub cursor: Option<String>,
170    /// Filters by room id.
171    pub room_id: Option<String>,
172    /// Filters by session id.
173    pub session_id: Option<String>,
174    /// Scopes to a specific user. Admin tokens only.
175    pub user_id: Option<String>,
176    /// Matches the room's meeting id.
177    pub query: Option<String>,
178}
179
180impl HlsListParams {
181    fn pagination(&self) -> ListParams {
182        ListParams {
183            page: self.page,
184            per_page: self.per_page,
185            cursor: self.cursor.clone(),
186        }
187    }
188}
189
190/// The HLS API. Reached via [`Client::hls`].
191#[derive(Debug, Clone, Copy)]
192pub struct HlsResource<'a> {
193    client: &'a Client,
194}
195
196impl<'a> HlsResource<'a> {
197    pub(crate) fn new(client: &'a Client) -> Self {
198        Self { client }
199    }
200
201    /// Begins HLS for a room.
202    pub async fn start(&self, room_id: &str, params: HlsStartParams) -> Result<EgressHandle> {
203        let mapped = composition_to_config(
204            params.composition.as_ref(),
205            // HLS requires a priority whenever a layout is present.
206            Some(LayoutPriority::Speaker),
207        );
208        // A custom composition layout overrides an explicitly-passed templateUrl.
209        let template_url = mapped.template_url.or(params.template_url);
210
211        let body = HlsStartWire {
212            room_id,
213            config: mapped.config,
214            template_url,
215            transcription: params.transcription.as_ref(),
216            webhook_url: params.webhook_url.as_deref(),
217            recording: params.recording.as_ref(),
218            resource_id: params.resource_id.as_deref(),
219        };
220        let path = format!("{PATH}/start");
221        let raw = self
222            .client
223            .maybe_json(Method::POST, &path, CallOptions::json(&body)?)
224            .await?;
225        Ok(to_egress_handle(EgressType::Hls, room_id, raw))
226    }
227
228    /// Stops HLS. Accepts the start handle, or a bare room id.
229    pub async fn stop(&self, target: impl Into<StopTarget>) -> Result<String> {
230        let target = target.into();
231        let path = format!("{PATH}/end");
232        self.client
233            .message(
234                Method::POST,
235                &path,
236                CallOptions::json(StopWire::from(&target))?,
237            )
238            .await
239    }
240
241    /// Fetches a room's currently-active HLS stream, with its playback URLs.
242    ///
243    /// Returns a not-found error when no stream is active.
244    pub async fn get(&self, room_id: &str) -> Result<HlsStream> {
245        let path = format!("{PATH}/{}/active", escape(room_id));
246        let raw: Value = self
247            .client
248            .json(Method::GET, &path, CallOptions::new())
249            .await?;
250
251        let data = raw.get("data").filter(|data| !data.is_null());
252        let Some(data) = data else {
253            return Err(Error::not_found(format!(
254                "no active HLS stream for room {room_id}"
255            )));
256        };
257
258        let mut stream: HlsStream =
259            serde_json::from_value(data.clone()).map_err(|e| Error::decode(path, e))?;
260        // This endpoint answers with the legacy `meetingId` alias.
261        if stream.room_id.is_none() {
262            stream.room_id = stream
263                .extra
264                .get("meetingId")
265                .and_then(Value::as_str)
266                .map(str::to_string);
267        }
268        Ok(stream)
269    }
270
271    /// Fetches an HLS stream by its id, as opposed to by room.
272    pub async fn get_by_id(&self, id: &str) -> Result<HlsStream> {
273        let path = format!("{PATH}/{}", escape(id));
274        self.client
275            .json(Method::GET, &path, CallOptions::new())
276            .await
277    }
278
279    /// Captures a thumbnail from the live HLS stream.
280    pub async fn capture(&self, params: HlsCaptureParams) -> Result<HlsCaptureResult> {
281        let path = format!("{PATH}/capture");
282        self.client
283            .json(Method::POST, &path, CallOptions::json(&params)?)
284            .await
285    }
286
287    /// Lists HLS streams, one page at a time.
288    pub async fn list(&self, params: HlsListParams) -> Result<Page<HlsStream>> {
289        paginate(self.fetcher(&params), &params.pagination(), "data", None).await
290    }
291
292    /// Lists HLS streams, transparently fetching every page.
293    pub fn list_stream(
294        &self,
295        params: HlsListParams,
296    ) -> impl Stream<Item = Result<HlsStream>> + Send {
297        auto_page(self.fetcher(&params), params.pagination(), "data", None)
298    }
299
300    /// Deletes a finished HLS recording.
301    pub async fn delete(&self, id: &str) -> Result<String> {
302        let path = format!("{PATH}/{}", escape(id));
303        self.client
304            .message(Method::DELETE, &path, CallOptions::new())
305            .await
306    }
307
308    fn fetcher(&self, params: &HlsListParams) -> PageFetcher {
309        let client = self.client.clone();
310        let params = params.clone();
311        Arc::new(move |page, per_page| {
312            let client = client.clone();
313            let params = params.clone();
314            Box::pin(async move {
315                let query = QueryBuilder::new()
316                    .opt("page", page)
317                    .opt("perPage", per_page)
318                    .opt_str("roomId", params.room_id.as_deref())
319                    .opt_str("sessionId", params.session_id.as_deref())
320                    .opt_str("userId", params.user_id.as_deref())
321                    .opt_str("query", params.query.as_deref())
322                    .into_pairs();
323                client
324                    .json::<Value>(Method::GET, PATH, CallOptions::new().query(query))
325                    .await
326            })
327        })
328    }
329}