1use 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/livestreams";
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct RtmpStream {
29 pub url: String,
31 pub stream_key: String,
33}
34
35#[derive(Debug, Clone, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct Livestream {
39 pub id: String,
41 pub composer_id: Option<String>,
43 pub room_id: Option<String>,
45 pub session_id: Option<String>,
47 #[serde(default, deserialize_with = "crate::common::null_to_default")]
49 pub outputs: Vec<RtmpStream>,
50 pub start: Option<String>,
52 pub end: Option<String>,
54 pub region: Option<String>,
56 pub webhook: Option<WebhookDeliverySummary>,
58 #[serde(default, deserialize_with = "crate::common::null_to_default")]
60 pub links: ResourceLinks,
61 #[serde(flatten)]
63 pub extra: Map<String, Value>,
64}
65
66#[derive(Debug, Clone, Default)]
68pub struct RtmpStartParams {
69 pub streams: Vec<RtmpStream>,
71 pub composition: Option<Composition>,
73 pub transcription: Option<TranscriptionConfig>,
75 pub resource_id: Option<String>,
77}
78
79#[derive(Debug, Serialize)]
81#[serde(rename_all = "camelCase")]
82struct RtmpStartWire<'a> {
83 room_id: &'a str,
84 outputs: &'a [RtmpStream],
85 #[serde(skip_serializing_if = "Option::is_none")]
86 config: Option<CompositionConfig>,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 template_url: Option<String>,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 transcription: Option<&'a TranscriptionConfig>,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 resource_id: Option<&'a str>,
93}
94
95#[derive(Debug, Clone, Default)]
97pub struct RtmpListParams {
98 pub page: Option<u32>,
100 pub per_page: Option<u32>,
102 pub cursor: Option<String>,
104 pub room_id: Option<String>,
106 pub session_id: Option<String>,
108 pub user_id: Option<String>,
110 pub query: Option<String>,
112}
113
114impl RtmpListParams {
115 fn pagination(&self) -> ListParams {
116 ListParams {
117 page: self.page,
118 per_page: self.per_page,
119 cursor: self.cursor.clone(),
120 }
121 }
122}
123
124#[derive(Debug, Clone, Copy)]
126pub struct RtmpResource<'a> {
127 client: &'a Client,
128}
129
130impl<'a> RtmpResource<'a> {
131 pub(crate) fn new(client: &'a Client) -> Self {
132 Self { client }
133 }
134
135 pub async fn start(&self, room_id: &str, params: RtmpStartParams) -> Result<EgressHandle> {
137 if params.streams.is_empty() {
138 return Err(Error::validation(
139 "rtmp.start() requires at least one destination in `streams`",
140 ));
141 }
142 let mapped = composition_to_config(
143 params.composition.as_ref(),
144 Some(LayoutPriority::Speaker),
146 );
147 let body = RtmpStartWire {
148 room_id,
149 outputs: ¶ms.streams,
150 config: mapped.config,
151 template_url: mapped.template_url,
152 transcription: params.transcription.as_ref(),
153 resource_id: params.resource_id.as_deref(),
154 };
155 let path = format!("{PATH}/start");
156 let raw = self
157 .client
158 .maybe_json(Method::POST, &path, CallOptions::json(&body)?)
159 .await?;
160 Ok(to_egress_handle(EgressType::Livestream, room_id, raw))
161 }
162
163 pub async fn stop(&self, target: impl Into<StopTarget>) -> Result<String> {
165 let target = target.into();
166 let path = format!("{PATH}/end");
167 self.client
168 .message(
169 Method::POST,
170 &path,
171 CallOptions::json(StopWire::from(&target))?,
172 )
173 .await
174 }
175
176 pub async fn list(&self, params: RtmpListParams) -> Result<Page<Livestream>> {
178 paginate(self.fetcher(¶ms), ¶ms.pagination(), "data", None).await
179 }
180
181 pub fn list_stream(
183 &self,
184 params: RtmpListParams,
185 ) -> impl Stream<Item = Result<Livestream>> + Send {
186 auto_page(self.fetcher(¶ms), params.pagination(), "data", None)
187 }
188
189 pub async fn get(&self, id: &str) -> Result<Livestream> {
191 let path = format!("{PATH}/{}", escape(id));
192 self.client
193 .json(Method::GET, &path, CallOptions::new())
194 .await
195 }
196
197 fn fetcher(&self, params: &RtmpListParams) -> PageFetcher {
198 let client = self.client.clone();
199 let params = params.clone();
200 Arc::new(move |page, per_page| {
201 let client = client.clone();
202 let params = params.clone();
203 Box::pin(async move {
204 let query = QueryBuilder::new()
205 .opt("page", page)
206 .opt("perPage", per_page)
207 .opt_str("roomId", params.room_id.as_deref())
208 .opt_str("sessionId", params.session_id.as_deref())
209 .opt_str("userId", params.user_id.as_deref())
210 .opt_str("query", params.query.as_deref())
211 .into_pairs();
212 client
213 .json::<Value>(Method::GET, PATH, CallOptions::new().query(query))
214 .await
215 })
216 })
217 }
218}