1use std::sync::Arc;
4
5use futures_util::Stream;
6use reqwest::Method;
7use serde::{Deserialize, Serialize, Serializer};
8use serde_json::{Map, Value};
9
10use crate::client::{CallOptions, Client};
11use crate::common::{string_enum, WebhookDeliverySummary};
12use crate::error::Result;
13use crate::pagination::{auto_page, paginate, ListParams, Page, PageFetcher};
14use crate::query::QueryBuilder;
15use crate::resources::escape;
16
17const PATH: &str = "/v2/transcodings";
18
19string_enum! {
20 TranscodingStatus {
22 PENDING => "pending",
24 PROCESSING => "processing",
26 COMPLETED => "completed",
28 FAILED => "failed",
30 CANCELLED => "cancelled",
32 }
33}
34
35string_enum! {
36 TranscodingTask {
38 COMPOSITE_MERGE => "composite-merge",
40 HLS_TO_MP4 => "hls-to-mp4",
42 MEETING_RECORDING_MERGE => "meeting-recording-merge",
44 }
45}
46
47#[derive(Debug, Clone, Default, Serialize)]
49#[serde(rename_all = "camelCase")]
50pub struct TranscodingWatermark {
51 #[serde(rename = "type")]
53 pub kind: String,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub image: Option<String>,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub start_time: Option<String>,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub lat: Option<f64>,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub long: Option<f64>,
66}
67
68#[derive(Debug, Clone, Default, Serialize)]
70#[serde(rename_all = "camelCase")]
71pub struct TranscodingStorage {
72 #[serde(rename = "type")]
74 pub kind: String,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub bucket: Option<String>,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub container: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub dir_path: Option<String>,
84}
85
86#[derive(Debug, Clone, Default, Serialize)]
88#[serde(rename_all = "camelCase")]
89pub struct MergeTranscodingParams {
90 pub recording_ids: Vec<String>,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub task: Option<String>,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub webhook_url: Option<String>,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub watermark: Option<TranscodingWatermark>,
102}
103
104#[derive(Debug, Clone, Default, Serialize)]
106#[serde(rename_all = "camelCase")]
107pub struct HlsToMp4Params {
108 #[serde(skip_serializing_if = "Option::is_none")]
111 pub room_id: Option<String>,
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub session_id: Option<String>,
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub hls_id: Option<String>,
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub webhook_url: Option<String>,
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub storage: Option<TranscodingStorage>,
124}
125
126#[derive(Debug, Clone)]
131pub struct MeetingRecordingRef {
132 pub id: String,
134 pub presigned_url: Option<String>,
136}
137
138impl MeetingRecordingRef {
139 pub fn new(id: impl Into<String>) -> Self {
141 Self {
142 id: id.into(),
143 presigned_url: None,
144 }
145 }
146}
147
148impl From<&str> for MeetingRecordingRef {
149 fn from(id: &str) -> Self {
150 Self::new(id)
151 }
152}
153
154impl From<String> for MeetingRecordingRef {
155 fn from(id: String) -> Self {
156 Self::new(id)
157 }
158}
159
160impl Serialize for MeetingRecordingRef {
161 fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
162 match &self.presigned_url {
163 None => serializer.serialize_str(&self.id),
164 Some(presigned_url) => {
165 #[derive(Serialize)]
166 #[serde(rename_all = "camelCase")]
167 struct Full<'a> {
168 id: &'a str,
169 presigned_url: &'a str,
170 }
171 Full {
172 id: &self.id,
173 presigned_url,
174 }
175 .serialize(serializer)
176 }
177 }
178 }
179}
180
181#[derive(Debug, Clone, Default, Serialize)]
183#[serde(rename_all = "camelCase")]
184pub struct MeetingRecordingMergeParams {
185 pub recording_ids: Vec<MeetingRecordingRef>,
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub webhook_url: Option<String>,
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub presigned_output_url: Option<String>,
193}
194
195#[derive(Debug, Clone, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct TranscodingFile {
199 pub id: Option<String>,
201 #[serde(rename = "type")]
203 pub kind: Option<String>,
204 pub size: Option<i64>,
206 pub meta: Option<Value>,
208 pub file_path: Option<String>,
210 pub file_url: Option<String>,
212}
213
214#[derive(Debug, Clone, Deserialize)]
216#[serde(rename_all = "camelCase")]
217pub struct Transcoding {
218 pub id: String,
220 #[serde(default, deserialize_with = "crate::common::null_to_default")]
222 pub recording_ids: Vec<String>,
223 pub room_id: Option<String>,
225 pub session_id: Option<String>,
227 pub hls_id: Option<String>,
229 pub status: Option<TranscodingStatus>,
231 pub task: Option<TranscodingTask>,
233 pub started_at: Option<String>,
235 pub stopped_at: Option<String>,
237 pub file: Option<TranscodingFile>,
239 pub webhook: Option<WebhookDeliverySummary>,
241 #[serde(flatten)]
243 pub extra: Map<String, Value>,
244}
245
246#[derive(Debug, Clone, Default)]
248pub struct ListTranscodingsParams {
249 pub page: Option<u32>,
251 pub per_page: Option<u32>,
253 pub cursor: Option<String>,
255 pub room_id: Option<String>,
257 pub session_id: Option<String>,
259 pub hls_id: Option<String>,
261 pub status: Option<TranscodingStatus>,
263}
264
265impl ListTranscodingsParams {
266 fn pagination(&self) -> ListParams {
267 ListParams {
268 page: self.page,
269 per_page: self.per_page,
270 cursor: self.cursor.clone(),
271 }
272 }
273}
274
275#[derive(Debug, Clone, Copy)]
277pub struct TranscodingsResource<'a> {
278 client: &'a Client,
279}
280
281impl<'a> TranscodingsResource<'a> {
282 pub(crate) fn new(client: &'a Client) -> Self {
283 Self { client }
284 }
285
286 pub async fn merge(&self, params: MergeTranscodingParams) -> Result<Transcoding> {
288 let path = format!("{PATH}/merge");
289 self.client
290 .json(Method::POST, &path, CallOptions::json(¶ms)?)
291 .await
292 }
293
294 pub async fn hls_to_mp4(&self, params: HlsToMp4Params) -> Result<Transcoding> {
296 let path = format!("{PATH}/hls-to-mp4");
297 self.client
298 .json(Method::POST, &path, CallOptions::json(¶ms)?)
299 .await
300 }
301
302 pub async fn meeting_recording_merge(
304 &self,
305 params: MeetingRecordingMergeParams,
306 ) -> Result<Transcoding> {
307 let path = format!("{PATH}/meeting-recording-merge");
308 self.client
309 .json(Method::POST, &path, CallOptions::json(¶ms)?)
310 .await
311 }
312
313 pub async fn list(&self, params: ListTranscodingsParams) -> Result<Page<Transcoding>> {
315 paginate(self.fetcher(¶ms), ¶ms.pagination(), "data", None).await
316 }
317
318 pub fn list_stream(
320 &self,
321 params: ListTranscodingsParams,
322 ) -> impl Stream<Item = Result<Transcoding>> + Send {
323 auto_page(self.fetcher(¶ms), params.pagination(), "data", None)
324 }
325
326 pub async fn get(&self, id: &str) -> Result<Transcoding> {
328 let path = format!("{PATH}/{}", escape(id));
329 self.client
330 .json(Method::GET, &path, CallOptions::new())
331 .await
332 }
333
334 pub async fn cancel(&self, id: &str) -> Result<Transcoding> {
336 let path = format!("{PATH}/{}/cancel", escape(id));
337 self.client
338 .json(Method::POST, &path, CallOptions::new())
339 .await
340 }
341
342 fn fetcher(&self, params: &ListTranscodingsParams) -> PageFetcher {
343 let client = self.client.clone();
344 let params = params.clone();
345 Arc::new(move |page, per_page| {
346 let client = client.clone();
347 let params = params.clone();
348 Box::pin(async move {
349 let query = QueryBuilder::new()
350 .opt("page", page)
351 .opt("perPage", per_page)
352 .opt_str("roomId", params.room_id.as_deref())
353 .opt_str("sessionId", params.session_id.as_deref())
354 .opt_str("hlsId", params.hls_id.as_deref())
355 .opt_str(
356 "status",
357 params.status.as_ref().map(TranscodingStatus::as_str),
358 )
359 .into_pairs();
360 client
361 .json::<Value>(Method::GET, PATH, CallOptions::new().query(query))
362 .await
363 })
364 })
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371 use serde_json::json;
372
373 #[test]
374 fn a_bare_recording_ref_serializes_as_a_string() {
375 let params = MeetingRecordingMergeParams {
376 recording_ids: vec!["rec-1".into(), "rec-2".into()],
377 ..Default::default()
378 };
379 assert_eq!(
380 serde_json::to_value(¶ms).unwrap(),
381 json!({"recordingIds": ["rec-1", "rec-2"]})
382 );
383 }
384
385 #[test]
386 fn a_recording_ref_with_a_presigned_url_serializes_as_an_object() {
387 let params = MeetingRecordingMergeParams {
388 recording_ids: vec![
389 MeetingRecordingRef::new("rec-1"),
390 MeetingRecordingRef {
391 id: "rec-2".into(),
392 presigned_url: Some("https://s3/get".into()),
393 },
394 ],
395 presigned_output_url: Some("https://s3/put".into()),
396 ..Default::default()
397 };
398 assert_eq!(
399 serde_json::to_value(¶ms).unwrap(),
400 json!({
401 "recordingIds": ["rec-1", {"id": "rec-2", "presignedUrl": "https://s3/get"}],
402 "presignedOutputUrl": "https://s3/put",
403 })
404 );
405 }
406}