use std::sync::Arc;
use futures_util::Stream;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::client::{CallOptions, Client};
use crate::common::{
CompositionConfig, LayoutPriority, ResourceLinks, TranscriptionConfig, WebhookDeliverySummary,
};
use crate::error::{Error, Result};
use crate::pagination::{auto_page, paginate, ListParams, Page, PageFetcher};
use crate::query::QueryBuilder;
use crate::resources::egress::{
composition_to_config, to_egress_handle, Composition, EgressHandle, EgressType, StopTarget,
StopWire,
};
use crate::resources::escape;
const PATH: &str = "/v2/hls";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HlsCaptureFormat {
Png,
Jpg,
Webp,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HlsStream {
pub id: String,
pub composer_id: Option<String>,
pub room_id: Option<String>,
pub session_id: Option<String>,
pub mode: Option<String>,
pub start: Option<String>,
pub end: Option<String>,
pub quality: Option<String>,
pub orientation: Option<String>,
#[serde(default, deserialize_with = "crate::common::null_to_default")]
pub capture_log: Vec<Value>,
pub encryption: Option<Map<String, Value>>,
pub duration: Option<f64>,
pub webhook: Option<WebhookDeliverySummary>,
#[serde(default, deserialize_with = "crate::common::null_to_default")]
pub links: ResourceLinks,
pub downstream_url: Option<String>,
pub playback_hls_url: Option<String>,
pub livestream_url: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, Default)]
pub struct HlsStartParams {
pub composition: Option<Composition>,
pub transcription: Option<TranscriptionConfig>,
pub webhook_url: Option<String>,
pub recording: Option<Value>,
pub template_url: Option<String>,
pub resource_id: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct HlsStartWire<'a> {
room_id: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
config: Option<CompositionConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
template_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
transcription: Option<&'a TranscriptionConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
webhook_url: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
recording: Option<&'a Value>,
#[serde(skip_serializing_if = "Option::is_none")]
resource_id: Option<&'a str>,
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HlsCaptureParams {
pub room_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<HlsCaptureFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time: Option<u32>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HlsCaptureResult {
pub message: String,
pub room_id: String,
pub file_path: Option<String>,
pub file_size: Option<i64>,
pub file_name: Option<String>,
pub meta: Option<Value>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, Default)]
pub struct HlsListParams {
pub page: Option<u32>,
pub per_page: Option<u32>,
pub cursor: Option<String>,
pub room_id: Option<String>,
pub session_id: Option<String>,
pub user_id: Option<String>,
pub query: Option<String>,
}
impl HlsListParams {
fn pagination(&self) -> ListParams {
ListParams {
page: self.page,
per_page: self.per_page,
cursor: self.cursor.clone(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct HlsResource<'a> {
client: &'a Client,
}
impl<'a> HlsResource<'a> {
pub(crate) fn new(client: &'a Client) -> Self {
Self { client }
}
pub async fn start(&self, room_id: &str, params: HlsStartParams) -> Result<EgressHandle> {
let mapped = composition_to_config(
params.composition.as_ref(),
Some(LayoutPriority::Speaker),
);
let template_url = mapped.template_url.or(params.template_url);
let body = HlsStartWire {
room_id,
config: mapped.config,
template_url,
transcription: params.transcription.as_ref(),
webhook_url: params.webhook_url.as_deref(),
recording: params.recording.as_ref(),
resource_id: params.resource_id.as_deref(),
};
let path = format!("{PATH}/start");
let raw = self
.client
.maybe_json(Method::POST, &path, CallOptions::json(&body)?)
.await?;
Ok(to_egress_handle(EgressType::Hls, room_id, raw))
}
pub async fn stop(&self, target: impl Into<StopTarget>) -> Result<String> {
let target = target.into();
let path = format!("{PATH}/end");
self.client
.message(
Method::POST,
&path,
CallOptions::json(StopWire::from(&target))?,
)
.await
}
pub async fn get(&self, room_id: &str) -> Result<HlsStream> {
let path = format!("{PATH}/{}/active", escape(room_id));
let raw: Value = self
.client
.json(Method::GET, &path, CallOptions::new())
.await?;
let data = raw.get("data").filter(|data| !data.is_null());
let Some(data) = data else {
return Err(Error::not_found(format!(
"no active HLS stream for room {room_id}"
)));
};
let mut stream: HlsStream =
serde_json::from_value(data.clone()).map_err(|e| Error::decode(path, e))?;
if stream.room_id.is_none() {
stream.room_id = stream
.extra
.get("meetingId")
.and_then(Value::as_str)
.map(str::to_string);
}
Ok(stream)
}
pub async fn get_by_id(&self, id: &str) -> Result<HlsStream> {
let path = format!("{PATH}/{}", escape(id));
self.client
.json(Method::GET, &path, CallOptions::new())
.await
}
pub async fn capture(&self, params: HlsCaptureParams) -> Result<HlsCaptureResult> {
let path = format!("{PATH}/capture");
self.client
.json(Method::POST, &path, CallOptions::json(¶ms)?)
.await
}
pub async fn list(&self, params: HlsListParams) -> Result<Page<HlsStream>> {
paginate(self.fetcher(¶ms), ¶ms.pagination(), "data", None).await
}
pub fn list_stream(
&self,
params: HlsListParams,
) -> impl Stream<Item = Result<HlsStream>> + Send {
auto_page(self.fetcher(¶ms), params.pagination(), "data", None)
}
pub async fn delete(&self, id: &str) -> Result<String> {
let path = format!("{PATH}/{}", escape(id));
self.client
.message(Method::DELETE, &path, CallOptions::new())
.await
}
fn fetcher(&self, params: &HlsListParams) -> PageFetcher {
let client = self.client.clone();
let params = params.clone();
Arc::new(move |page, per_page| {
let client = client.clone();
let params = params.clone();
Box::pin(async move {
let query = QueryBuilder::new()
.opt("page", page)
.opt("perPage", per_page)
.opt_str("roomId", params.room_id.as_deref())
.opt_str("sessionId", params.session_id.as_deref())
.opt_str("userId", params.user_id.as_deref())
.opt_str("query", params.query.as_deref())
.into_pairs();
client
.json::<Value>(Method::GET, PATH, CallOptions::new().query(query))
.await
})
})
}
}