Skip to main content

moq_ffi/
session.rs

1use std::sync::Arc;
2
3use moq_net::Session;
4use url::Url;
5
6use crate::error::MoqError;
7use crate::ffi::Task;
8use crate::origin::MoqOriginProducer;
9
10struct Client {
11	config: moq_native::ClientConfig,
12	publish: Option<Arc<MoqOriginProducer>>,
13	consume: Option<Arc<MoqOriginProducer>>,
14}
15
16impl Client {
17	async fn connect(&self, url: Url) -> Result<Arc<MoqSession>, MoqError> {
18		let client = self.config.clone().init().map_err(map_connect_error)?;
19
20		let publish = self.publish.as_ref().map(|o| o.inner().consume());
21		let consume = self.consume.as_ref().map(|o| o.inner().clone());
22
23		let session = client
24			.with_publish(publish)
25			.with_consume(consume)
26			.connect(url)
27			.await
28			.map_err(map_connect_error)?;
29
30		Ok(Arc::new(MoqSession::new(session)))
31	}
32}
33
34fn map_connect_error(err: moq_native::Error) -> MoqError {
35	match err.connect_error() {
36		Some(moq_native::ConnectError::Unauthorized) => MoqError::Unauthorized,
37		Some(moq_native::ConnectError::Forbidden) => MoqError::Forbidden,
38		_ => MoqError::Connect(format!("{err}")),
39	}
40}
41
42#[cfg(test)]
43mod tests {
44	use super::*;
45
46	#[test]
47	fn maps_native_auth_connect_errors() {
48		assert!(matches!(
49			map_connect_error(moq_native::ConnectError::Unauthorized.into()),
50			MoqError::Unauthorized
51		));
52		assert!(matches!(
53			map_connect_error(moq_native::ConnectError::Forbidden.into()),
54			MoqError::Forbidden
55		));
56	}
57}
58
59#[derive(uniffi::Object)]
60pub struct MoqClient {
61	task: Task<Client>,
62}
63
64#[uniffi::export]
65impl MoqClient {
66	/// Create a new MoQ client with default configuration.
67	#[uniffi::constructor]
68	pub fn new() -> Arc<Self> {
69		let _guard = crate::ffi::RUNTIME.enter();
70		Arc::new(Self {
71			task: Task::new(Client {
72				config: moq_native::ClientConfig::default(),
73				publish: None,
74				consume: None,
75			}),
76		})
77	}
78
79	/// Disable TLS certificate verification (for development only).
80	pub fn set_tls_disable_verify(&self, disable: bool) {
81		if let Some(mut state) = self.task.lock() {
82			state.config.tls.disable_verify = Some(disable);
83		}
84	}
85
86	/// Trust these PEM root certificate file(s) instead of the system roots.
87	///
88	/// Pass the paths to PEM-encoded CA certificates. An empty list restores the
89	/// default behavior of using the platform's native root store.
90	pub fn set_tls_roots(&self, paths: Vec<String>) {
91		if let Some(mut state) = self.task.lock() {
92			state.config.tls.root = paths.into_iter().map(Into::into).collect();
93		}
94	}
95
96	/// Pin the peer to a certificate with one of these SHA-256 fingerprints, encoded as hex.
97	///
98	/// This is the native equivalent of the browser's WebTransport `serverCertificateHashes`
99	/// and accepts the same values a server reports (see `MoqServer.cert_fingerprints`). Use it
100	/// to trust a self-signed certificate without disabling verification. An empty list clears
101	/// any pinned fingerprints.
102	pub fn set_tls_fingerprints(&self, fingerprints: Vec<String>) {
103		if let Some(mut state) = self.task.lock() {
104			state.config.tls.fingerprint = fingerprints;
105		}
106	}
107
108	/// Set the local UDP socket bind address. Defaults to `[::]:0`.
109	///
110	/// Returns an error if the address cannot be parsed.
111	pub fn set_bind(&self, addr: String) -> Result<(), MoqError> {
112		let parsed: std::net::SocketAddr = addr
113			.parse()
114			.map_err(|err| MoqError::Bind(format!("invalid bind address: {err}")))?;
115		if let Some(mut state) = self.task.lock() {
116			state.config.bind = parsed;
117		}
118		Ok(())
119	}
120
121	/// Set the origin to publish local broadcasts to the remote.
122	pub fn set_publish(&self, origin: Option<Arc<MoqOriginProducer>>) {
123		if let Some(mut state) = self.task.lock() {
124			state.publish = origin;
125		}
126	}
127
128	/// Set the origin to consume remote broadcasts from the remote.
129	pub fn set_consume(&self, origin: Option<Arc<MoqOriginProducer>>) {
130		if let Some(mut state) = self.task.lock() {
131			state.consume = origin;
132		}
133	}
134
135	/// Connect to a MoQ server and wait for the session to be established.
136	///
137	/// Can be cancelled by calling `cancel()`.
138	pub async fn connect(&self, url: String) -> Result<Arc<MoqSession>, MoqError> {
139		let url = Url::parse(&url)?;
140
141		self.task.run(|state| async move { state.connect(url).await }).await
142	}
143
144	/// Cancel all current and future `connect()` calls.
145	pub fn cancel(&self) {
146		self.task.cancel();
147	}
148}
149
150#[derive(uniffi::Object)]
151pub struct MoqSession {
152	inner: Option<moq_net::Session>,
153	closed: Task<Session>,
154}
155
156impl MoqSession {
157	pub(crate) fn new(session: moq_net::Session) -> Self {
158		Self {
159			inner: Some(session.clone()),
160			closed: Task::new(session),
161		}
162	}
163}
164
165impl Drop for MoqSession {
166	fn drop(&mut self) {
167		let _guard = crate::ffi::RUNTIME.enter();
168		self.inner.take();
169	}
170}
171
172#[uniffi::export]
173impl MoqSession {
174	/// Wait until the session is closed.
175	pub async fn closed(&self) -> Result<(), MoqError> {
176		// We have a task to run all of the closed calls juuuuust so they use the same tokio runtime.
177		self.closed
178			.run(|session| async move { session.closed().await.map_err(Into::into) })
179			.await
180	}
181
182	/// Close the session with the given error code.
183	pub fn cancel(&self, code: u32) {
184		let _guard = crate::ffi::RUNTIME.enter();
185		if let Some(inner) = &self.inner {
186			inner.clone().close(moq_net::Error::Remote(code));
187		}
188		// NOTE: we don't abort the closed Task because it will be aborted via above ^
189		// We'll get a slightly better error message instead of Cancelled.
190	}
191
192	/// Graceful shutdown. Equivalent to `cancel(0)`. Documents the
193	/// convention that code 0 means "no error" so callers don't have to
194	/// pick one. Named `shutdown` (not `close`) because UniFFI's Kotlin
195	/// generator already emits an `AutoCloseable.close()` that releases
196	/// the FFI handle, and shadowing it would silently mean a different
197	/// thing per binding.
198	pub fn shutdown(&self) {
199		self.cancel(0);
200	}
201}