Skip to main content

moq/
api.rs

1use crate::{Connect, Error, State, ffi};
2
3use std::ffi::c_char;
4use std::ffi::c_void;
5use std::str::FromStr;
6
7use tracing::Level;
8
9/// Information about a video rendition in the catalog.
10#[repr(C)]
11#[allow(non_camel_case_types)]
12pub struct moq_video_config {
13	/// The name of the track, NOT NULL terminated.
14	pub name: *const c_char,
15	pub name_len: usize,
16
17	/// The codec of the track, NOT NULL terminated
18	pub codec: *const c_char,
19	pub codec_len: usize,
20
21	/// The description of the track, or NULL if not used.
22	/// This is codec specific, for example H264:
23	///   - NULL: annex.b encoded
24	///   - Non-NULL: AVCC encoded
25	pub description: *const u8,
26	pub description_len: usize,
27
28	/// The encoded width/height of the media, or NULL if not available
29	pub coded_width: *const u32,
30	pub coded_height: *const u32,
31}
32
33/// Catalog properties shared by every video rendition.
34///
35/// A false `has_*` flag clears that field from the next catalog rather than preserving its previous value.
36#[repr(C)]
37#[allow(non_camel_case_types)]
38#[derive(Clone, Copy, Default)]
39pub struct moq_video_properties {
40	/// Final rendered width in pixels when `has_display` is true.
41	pub display_width: u32,
42
43	/// Final rendered height in pixels when `has_display` is true.
44	pub display_height: u32,
45
46	/// Whether `display_width` and `display_height` are present.
47	pub has_display: bool,
48
49	/// Clockwise rotation in degrees when `has_rotation` is true.
50	pub rotation: f64,
51
52	/// Whether `rotation` is present.
53	pub has_rotation: bool,
54
55	/// Whether to flip horizontally after rotation when `has_flip` is true.
56	pub flip: bool,
57
58	/// Whether `flip` is present.
59	pub has_flip: bool,
60}
61
62/// Information about an audio rendition in the catalog.
63#[repr(C)]
64#[allow(non_camel_case_types)]
65pub struct moq_audio_config {
66	/// The name of the track, NOT NULL terminated
67	pub name: *const c_char,
68	pub name_len: usize,
69
70	/// The codec of the track, NOT NULL terminated
71	pub codec: *const c_char,
72	pub codec_len: usize,
73
74	/// The description of the track, or NULL if not used.
75	pub description: *const u8,
76	pub description_len: usize,
77
78	/// The sample rate of the track in Hz
79	pub sample_rate: u32,
80
81	/// The number of channels in the track
82	pub channel_count: u32,
83}
84
85/// Options for a JSON snapshot track (lossy latest-value mode).
86///
87/// The same config is passed to a producer and its consumers, but the consumer reads only
88/// `compression`; `delta_ratio` is producer-only.
89#[repr(C)]
90#[allow(non_camel_case_types)]
91pub struct moq_json_snapshot_config {
92	/// How aggressively the producer emits deltas instead of full snapshots. `0` disables deltas
93	/// (one snapshot per group); a positive value allows roughly that many snapshots' worth of
94	/// deltas before rolling. Ignored by the consumer.
95	pub delta_ratio: u32,
96
97	/// DEFLATE-compress each group. Must match on the producer and consumer.
98	pub compression: bool,
99}
100
101/// Options for a JSON stream track (lossless append-log mode).
102#[repr(C)]
103#[allow(non_camel_case_types)]
104pub struct moq_json_stream_config {
105	/// DEFLATE-compress the group. Must match on the producer and consumer.
106	pub compression: bool,
107}
108
109/// A JSON value delivered by a consumer callback.
110#[repr(C)]
111#[allow(non_camel_case_types)]
112pub struct moq_json_value {
113	/// The JSON document as UTF-8, NOT NULL terminated.
114	pub json: *const c_char,
115	pub json_len: usize,
116}
117
118/// Information about a frame of media.
119#[repr(C)]
120#[allow(non_camel_case_types)]
121pub struct moq_frame {
122	/// The payload of the frame, or NULL/0 if the stream has ended
123	pub payload: *const u8,
124	pub payload_size: usize,
125
126	/// The presentation timestamp of the frame in microseconds
127	pub timestamp_us: u64,
128
129	/// Whether the frame is a keyframe, aka the start of a new group.
130	pub keyframe: bool,
131}
132
133/// A best-effort raw track datagram delivered via [moq_consume_datagrams].
134#[repr(C)]
135#[allow(non_camel_case_types)]
136pub struct moq_datagram {
137	/// The payload of the datagram, or NULL/0 if the track has ended.
138	pub payload: *const u8,
139	pub payload_size: usize,
140
141	/// The presentation timestamp of the datagram in microseconds.
142	pub timestamp_us: u64,
143
144	/// Per-track sequence number, drawn from the same namespace as groups.
145	pub sequence: u64,
146}
147
148/// Publisher-side raw track properties.
149///
150/// A null [moq_publish_track] `info` pointer uses the moq-net defaults.
151/// A zero-initialized struct also uses those defaults, except `priority` where
152/// zero is the default itself.
153#[repr(C)]
154#[allow(non_camel_case_types)]
155pub struct moq_track_info {
156	/// Priority, used to break ties between subscriptions of equal subscriber priority.
157	pub priority: u8,
158
159	/// Whether groups are prioritized in sequence order.
160	/// Groups may always arrive out-of-order (or not at all) over the network.
161	pub ordered: bool,
162
163	/// Maximum age of a non-latest group before the publisher evicts it, in milliseconds.
164	/// The publisher-side half of `moq_subscription.latency_max_ms`.
165	pub latency_max_ms: u64,
166	/// Whether `latency_max_ms` should override the default.
167	pub latency_max_valid: bool,
168
169	/// Per-frame timescale in ticks per second.
170	pub timescale: u64,
171	/// Whether `timescale` should override the default microsecond timescale,
172	/// which matches the `timestamp_us` units used everywhere else in this ABI.
173	pub timescale_valid: bool,
174}
175
176impl TryFrom<&moq_track_info> for moq_net::track::Info {
177	type Error = Error;
178
179	fn try_from(info: &moq_track_info) -> Result<Self, Self::Error> {
180		// Raw tracks default to a microsecond timescale, matching the C ABI's
181		// timestamp_us units. An explicit timescale below overrides it.
182		let mut out = moq_net::track::Info::default()
183			.with_timescale(moq_net::Timescale::MICRO)
184			.with_priority(info.priority)
185			.with_ordered(info.ordered);
186		if info.latency_max_valid {
187			out = out.with_latency_max(std::time::Duration::from_millis(info.latency_max_ms));
188		}
189		if info.timescale_valid {
190			out = out.with_timescale(moq_net::Timescale::new(info.timescale)?);
191		}
192		Ok(out)
193	}
194}
195
196/// Subscriber-side raw track delivery preferences.
197///
198/// A null [moq_consume_track] or [moq_consume_track_update] `subscription`
199/// pointer uses the moq-net defaults.
200#[repr(C)]
201#[allow(non_camel_case_types)]
202pub struct moq_subscription {
203	/// Delivery priority. Higher values preempt lower ones under contention.
204	pub priority: u8,
205
206	/// Whether groups are prioritized in sequence order.
207	/// Groups may always arrive out-of-order (or not at all) over the network.
208	pub ordered: bool,
209
210	/// Maximum age of a non-latest group before it is skipped, in milliseconds.
211	/// Zero skips immediately. Enforced by the publisher's cache and by any local buffering.
212	pub latency_max_ms: u64,
213
214	/// First group to deliver.
215	pub group_start: u64,
216	/// Whether `group_start` is present. When false, delivery starts at the latest group.
217	pub group_start_valid: bool,
218
219	/// Last group to deliver, inclusive.
220	pub group_end: u64,
221	/// Whether `group_end` is present. When false, there is no end cap.
222	pub group_end_valid: bool,
223}
224
225impl From<&moq_subscription> for moq_net::track::Subscription {
226	fn from(subscription: &moq_subscription) -> Self {
227		let mut out = moq_net::track::Subscription::default()
228			.with_priority(subscription.priority)
229			.with_ordered(subscription.ordered)
230			.with_latency_max(std::time::Duration::from_millis(subscription.latency_max_ms));
231		if subscription.group_start_valid {
232			out = out.with_group_start(subscription.group_start);
233		}
234		if subscription.group_end_valid {
235			out = out.with_group_end(subscription.group_end);
236		}
237		out
238	}
239}
240
241/// A borrowed UTF-8 string slice, NOT NULL terminated.
242///
243/// Used in both directions. As an output (e.g. a JSON document libmoq hands back) the
244/// pointer borrows libmoq's own storage and is only valid until the owning resource is
245/// freed; see the function that fills it for the exact lifetime. As an input (e.g. a
246/// `moq_client_set_*` list) the pointer borrows the caller's storage and is only read
247/// during the call.
248#[repr(C)]
249#[allow(non_camel_case_types)]
250#[derive(Clone, Copy)]
251pub struct moq_string {
252	/// Pointer to `len` bytes of UTF-8, NOT NULL terminated.
253	pub data: *const c_char,
254	pub len: usize,
255}
256
257/// One untyped application catalog section: a name and its JSON value.
258///
259/// Both `name` and `json` are UTF-8, NOT NULL terminated, and borrow the catalog
260/// snapshot's storage. They stay valid until the snapshot is freed with
261/// [moq_consume_catalog_free]. `json` is the section's value serialized as JSON
262/// (parse it yourself); a top-level catalog key beyond `video`/`audio`.
263#[repr(C)]
264#[allow(non_camel_case_types)]
265pub struct moq_section {
266	/// The section name, NOT NULL terminated.
267	pub name: *const c_char,
268	pub name_len: usize,
269
270	/// The section value as a JSON document, NOT NULL terminated.
271	pub json: *const c_char,
272	pub json_len: usize,
273}
274
275/// Information about a broadcast announced by an origin.
276#[repr(C)]
277#[allow(non_camel_case_types)]
278pub struct moq_announced {
279	/// The path of the broadcast, NOT NULL terminated
280	pub path: *const c_char,
281	pub path_len: usize,
282
283	/// Whether the broadcast is active or has ended
284	/// This MUST toggle between true and false over the lifetime of the broadcast
285	pub active: bool,
286}
287
288/// A snapshot of connection statistics, filled in by [moq_session_stats].
289///
290/// Each metric has a `*_valid` flag: when `false`, the matching value is meaningless because
291/// the transport backend doesn't report it (a `false` flag is NOT the same as a zero value).
292/// Native QUIC reports every metric; the browser WebTransport reports few or none. Initialize
293/// the struct to zero before the call; [moq_session_stats] overwrites every field.
294#[repr(C)]
295#[allow(non_camel_case_types)]
296pub struct moq_connection_stats {
297	/// Smoothed round-trip time, in microseconds.
298	pub rtt_us: u64,
299	pub rtt_valid: bool,
300
301	/// Estimated send bandwidth from the congestion controller, in bits per second.
302	pub send_rate_bps: u64,
303	pub send_rate_valid: bool,
304
305	/// Estimated receive bandwidth from MoQ PROBE, in bits per second.
306	pub recv_rate_bps: u64,
307	pub recv_rate_valid: bool,
308
309	/// Total bytes sent, including retransmissions and overhead.
310	pub bytes_sent: u64,
311	pub bytes_sent_valid: bool,
312
313	/// Total bytes received, including duplicates and overhead.
314	pub bytes_received: u64,
315	pub bytes_received_valid: bool,
316
317	/// Total bytes lost (detected via retransmission or acknowledgement).
318	pub bytes_lost: u64,
319	pub bytes_lost_valid: bool,
320
321	/// Total datagrams sent.
322	pub packets_sent: u64,
323	pub packets_sent_valid: bool,
324
325	/// Total datagrams received.
326	pub packets_received: u64,
327	pub packets_received_valid: bool,
328
329	/// Total datagrams detected as lost.
330	pub packets_lost: u64,
331	pub packets_lost_valid: bool,
332}
333
334impl From<&moq_net::ConnectionStats> for moq_connection_stats {
335	fn from(stats: &moq_net::ConnectionStats) -> Self {
336		// An Option<u64> becomes a (value, valid) pair; absent metrics report 0/false.
337		fn split(value: Option<u64>) -> (u64, bool) {
338			(value.unwrap_or(0), value.is_some())
339		}
340
341		let (rtt_us, rtt_valid) = split(stats.rtt.map(|d| d.as_micros() as u64));
342		let (send_rate_bps, send_rate_valid) = split(stats.estimated_send_rate);
343		let (recv_rate_bps, recv_rate_valid) = split(stats.estimated_recv_rate);
344		let (bytes_sent, bytes_sent_valid) = split(stats.bytes_sent);
345		let (bytes_received, bytes_received_valid) = split(stats.bytes_received);
346		let (bytes_lost, bytes_lost_valid) = split(stats.bytes_lost);
347		let (packets_sent, packets_sent_valid) = split(stats.packets_sent);
348		let (packets_received, packets_received_valid) = split(stats.packets_received);
349		let (packets_lost, packets_lost_valid) = split(stats.packets_lost);
350
351		Self {
352			rtt_us,
353			rtt_valid,
354			send_rate_bps,
355			send_rate_valid,
356			recv_rate_bps,
357			recv_rate_valid,
358			bytes_sent,
359			bytes_sent_valid,
360			bytes_received,
361			bytes_received_valid,
362			bytes_lost,
363			bytes_lost_valid,
364			packets_sent,
365			packets_sent_valid,
366			packets_received,
367			packets_received_valid,
368			packets_lost,
369			packets_lost_valid,
370		}
371	}
372}
373
374/// Initialize the library with a log level.
375///
376/// This should be called before any other functions.
377/// The log_level is a string: "error", "warn", "info", "debug", "trace"
378///
379/// Returns a zero on success, or a negative code on failure.
380///
381/// # Safety
382/// - The caller must ensure that level is a valid pointer to level_len bytes of data.
383#[unsafe(no_mangle)]
384pub unsafe extern "C" fn moq_log_level(level: *const c_char, level_len: usize) -> i32 {
385	ffi::enter(move || {
386		match unsafe { ffi::parse_str(level, level_len)? } {
387			"" => moq_native::Log::default(),
388			level => moq_native::Log::new(Level::from_str(level)?),
389		}
390		.init()?;
391
392		Ok(())
393	})
394}
395
396/// Human-readable reason for the most recent failed call on the calling thread.
397///
398/// libmoq functions return only a negative code; this exposes the matching message
399/// (including detail the code can't carry, e.g. which URL failed to parse or why a
400/// decode failed). The string is only meaningful after a call returned a negative
401/// code; check the code first.
402///
403/// Returns a NUL-terminated, UTF-8 pointer valid until the next libmoq call **on the
404/// same thread**, or NULL if no error has been recorded on this thread. Copy it if you
405/// need it to outlive the next call. Errors delivered through status callbacks carry
406/// their code directly; read this from inside the callback to get their reason.
407#[unsafe(no_mangle)]
408pub extern "C" fn moq_error() -> *const c_char {
409	ffi::last_error_ptr()
410}
411
412/// The protocol version names this build offers by default, spelled the way
413/// [moq_client_set_versions] expects. Built once; the slices are valid for the life of
414/// the process.
415static VERSION_NAMES: std::sync::LazyLock<Vec<String>> =
416	std::sync::LazyLock::new(|| moq_net::Versions::all().iter().map(|v| v.to_string()).collect());
417
418/// List the protocol versions offered during the handshake by default.
419///
420/// Writes up to `count` names into `dst` and returns the total number available, which
421/// may be larger than `count`. Pass a NULL `dst` with a zero `count` to size the array
422/// first. Each name borrows a static string valid for the life of the process, so a
423/// caller building a menu can hold them indefinitely.
424///
425/// Work-in-progress versions are omitted, since they are not advertised unless pinned;
426/// [moq_client_set_versions] still accepts them by name.
427///
428/// Returns the total count on success, or a negative code on failure.
429///
430/// # Safety
431/// - The caller must ensure that `dst` is either NULL with a zero `count`, or a valid
432///   pointer to `count` writable [moq_string] values.
433#[unsafe(no_mangle)]
434pub unsafe extern "C" fn moq_versions(dst: *mut moq_string, count: usize) -> i32 {
435	ffi::enter(move || {
436		if !dst.is_null() {
437			let dst = unsafe { std::slice::from_raw_parts_mut(dst, count) };
438			for (slot, name) in dst.iter_mut().zip(VERSION_NAMES.iter()) {
439				slot.data = name.as_ptr().cast::<c_char>();
440				slot.len = name.len();
441			}
442		} else if count != 0 {
443			return Err(Error::InvalidPointer);
444		}
445
446		Ok(VERSION_NAMES.len())
447	})
448}
449
450/// The QUIC backend names this build offers, spelled the way [moq_client_set_backend]
451/// expects. Built once; the slices are valid for the life of the process.
452static BACKEND_NAMES: std::sync::LazyLock<Vec<&'static str>> =
453	std::sync::LazyLock::new(|| moq_native::QuicBackend::compiled().iter().map(|b| b.as_str()).collect());
454
455/// List the QUIC backends this build was compiled with.
456///
457/// Writes up to `count` names into `dst` and returns the total number available, which
458/// may be larger than `count`. Pass a NULL `dst` with a zero `count` to size the array
459/// first. Each name borrows a static string valid for the life of the process.
460///
461/// The backends are compile-time optional, so a caller building a menu must read this
462/// rather than listing names: an option this build lacks is rejected by
463/// [moq_client_set_backend], which would leave a menu entry that can only fail.
464///
465/// Returns the total count on success, or a negative code on failure.
466///
467/// # Safety
468/// - The caller must ensure that `dst` is either NULL with a zero `count`, or a valid
469///   pointer to `count` writable [moq_string] values.
470#[unsafe(no_mangle)]
471pub unsafe extern "C" fn moq_backends(dst: *mut moq_string, count: usize) -> i32 {
472	ffi::enter(move || {
473		if !dst.is_null() {
474			let dst = unsafe { std::slice::from_raw_parts_mut(dst, count) };
475			for (slot, name) in dst.iter_mut().zip(BACKEND_NAMES.iter()) {
476				slot.data = name.as_ptr().cast::<c_char>();
477				slot.len = name.len();
478			}
479		} else if count != 0 {
480			return Err(Error::InvalidPointer);
481		}
482
483		Ok(BACKEND_NAMES.len())
484	})
485}
486
487/// Whether this build can capture qlog traces.
488///
489/// Capture is compile-time optional. [moq_client_set_quic_qlog] accepts a directory
490/// either way, but dialing fails when the support is absent, so a caller offering the
491/// knob should hide it rather than surface an option that cannot work.
492#[unsafe(no_mangle)]
493pub extern "C" fn moq_qlog_supported() -> bool {
494	moq_native::qlog_supported()
495}
496
497/// A duration as the milliseconds the setters take, saturating rather than wrapping.
498fn millis(duration: std::time::Duration) -> u64 {
499	duration.as_millis().min(u64::MAX as u128) as u64
500}
501
502/// Create a client configuration for [moq_client_connect].
503///
504/// A fresh handle carries the same defaults [moq_session_connect] dials with; the
505/// `moq_client_set_*` functions override one knob at a time. Connecting clones the
506/// config, so one handle can open any number of sessions and stays editable in between.
507///
508/// Returns a non-zero handle on success, or a negative code on failure. Release it with
509/// [moq_client_close]; that does not disturb sessions already dialed from it.
510#[unsafe(no_mangle)]
511pub extern "C" fn moq_client_create() -> i32 {
512	ffi::enter(move || State::lock().client.create())
513}
514
515/// Release a client configuration created by [moq_client_create].
516///
517/// Sessions already dialed from it keep running: each connect took its own copy.
518///
519/// Returns zero on success, or a negative code if the handle is unknown.
520#[unsafe(no_mangle)]
521pub extern "C" fn moq_client_close(client: u32) -> i32 {
522	ffi::enter(move || {
523		let client = ffi::parse_id(client)?;
524		State::lock().client.close(client)
525	})
526}
527
528/// Restrict the protocol versions offered during the handshake.
529///
530/// By default every supported version is offered and the server picks one. Pass a
531/// subset to pin the negotiation, in the same spelling the CLI uses: `moq-lite-01`
532/// through `moq-lite-06-wip`, or `moq-transport-14` through `moq-transport-19`. An
533/// empty list restores the default.
534///
535/// Returns zero on success, or a negative code if the handle is unknown or a version
536/// string is unrecognized.
537///
538/// # Safety
539/// - The caller must ensure that `versions` is either NULL with a zero `count`, or a
540///   valid pointer to `count` [moq_string] values, each valid for its own length.
541#[unsafe(no_mangle)]
542pub unsafe extern "C" fn moq_client_set_versions(client: u32, versions: *const moq_string, count: usize) -> i32 {
543	ffi::enter(move || {
544		let client = ffi::parse_id(client)?;
545		let versions = unsafe { ffi::parse_strings(versions, count)? }
546			.into_iter()
547			.map(|version| moq_net::Version::from_str(&version).map_err(Error::InvalidConfig))
548			.collect::<Result<Vec<_>, Error>>()?;
549
550		State::lock().client.get_mut(client)?.version = versions;
551		Ok(())
552	})
553}
554
555/// Choose the QUIC backend: `"quinn"`, `"quiche"`, or `"noq"`.
556///
557/// Defaults to whichever is compiled in, preferring quinn. A NULL or empty value
558/// restores that auto-detection.
559///
560/// Returns zero on success, or a negative code if the handle is unknown or the backend
561/// is unrecognized (which includes a backend this build was compiled without).
562///
563/// # Safety
564/// - The caller must ensure that `backend` is NULL or a valid pointer to `backend_len` bytes.
565#[unsafe(no_mangle)]
566pub unsafe extern "C" fn moq_client_set_backend(client: u32, backend: *const c_char, backend_len: usize) -> i32 {
567	ffi::enter(move || {
568		let backend = match unsafe { ffi::parse_str_optional(backend, backend_len)? } {
569			Some(backend) => Some(moq_native::QuicBackend::from_str(backend).map_err(Error::InvalidConfig)?),
570			None => None,
571		};
572
573		let client = ffi::parse_id(client)?;
574		State::lock().client.get_mut(client)?.backend = backend;
575		Ok(())
576	})
577}
578
579/// Set the local UDP socket address to bind, e.g. `"[::]:0"` (the default) or
580/// `"192.0.2.7:0"` to pin the outgoing interface.
581///
582/// Returns zero on success, or a negative code if the handle is unknown or the address
583/// does not parse.
584///
585/// # Safety
586/// - The caller must ensure that `addr` is a valid pointer to `addr_len` bytes.
587#[unsafe(no_mangle)]
588pub unsafe extern "C" fn moq_client_set_bind(client: u32, addr: *const c_char, addr_len: usize) -> i32 {
589	ffi::enter(move || {
590		let addr = unsafe { ffi::parse_str(addr, addr_len)? };
591		let addr: std::net::SocketAddr = addr
592			.parse()
593			.map_err(|err| Error::InvalidConfig(format!("invalid bind address {addr:?}: {err}")))?;
594
595		let client = ffi::parse_id(client)?;
596		State::lock().client.get_mut(client)?.bind = addr;
597		Ok(())
598	})
599}
600
601/// Bound one connection attempt, covering both the dial and the MoQ handshake, in
602/// milliseconds.
603///
604/// Defaults to 30s; zero waits forever. The reconnect loop only re-arms its backoff
605/// between attempts, so this is what stops a peer that accepts the connection and then
606/// never speaks from wedging the loop.
607///
608/// Returns zero on success, or a negative code if the handle is unknown.
609#[unsafe(no_mangle)]
610pub extern "C" fn moq_client_set_connect_timeout(client: u32, timeout_ms: u64) -> i32 {
611	ffi::enter(move || {
612		let client = ffi::parse_id(client)?;
613		State::lock().client.get_mut(client)?.timeout = Some(std::time::Duration::from_millis(timeout_ms));
614		Ok(())
615	})
616}
617
618/// Delay before also dialing the next resolved address (Happy Eyeballs), in milliseconds.
619///
620/// When DNS returns several addresses, attempts alternate between IPv6 and IPv4, each
621/// starting this long after the previous one, and the first to complete wins. Defaults
622/// to 250ms; zero dials every address at once.
623///
624/// Returns zero on success, or a negative code if the handle is unknown.
625#[unsafe(no_mangle)]
626pub extern "C" fn moq_client_set_failover_delay(client: u32, delay_ms: u64) -> i32 {
627	ffi::enter(move || {
628		let client = ffi::parse_id(client)?;
629		State::lock().client.get_mut(client)?.failover_delay = Some(std::time::Duration::from_millis(delay_ms));
630		Ok(())
631	})
632}
633
634/// Delay before dialing an IPv4 address while the full DNS answer is outstanding, in
635/// milliseconds.
636///
637/// A dial runs the usual all-families lookup alongside an IPv4-only one that answers
638/// without waiting for the AAAA record, and starts on the first answer. The full answer
639/// is authoritative, including which family to try first, so this is how long the
640/// IPv4-only one waits for it before going ahead alone. Defaults to 50ms; zero dials as
641/// soon as any address resolves.
642///
643/// Returns zero on success, or a negative code if the handle is unknown.
644#[unsafe(no_mangle)]
645pub extern "C" fn moq_client_set_resolution_delay(client: u32, delay_ms: u64) -> i32 {
646	ffi::enter(move || {
647		let client = ffi::parse_id(client)?;
648		State::lock().client.get_mut(client)?.resolution_delay = Some(std::time::Duration::from_millis(delay_ms));
649		Ok(())
650	})
651}
652
653/// Delay before racing a WebSocket fallback against the QUIC dial, in milliseconds.
654///
655/// Defaults to 200ms, and drops to zero for a server WebSocket already won against.
656/// This is what gets a publisher through a network that blocks UDP.
657///
658/// Returns zero on success, or a negative code if the handle is unknown.
659#[unsafe(no_mangle)]
660pub extern "C" fn moq_client_set_websocket_delay(client: u32, delay_ms: u64) -> i32 {
661	ffi::enter(move || {
662		let client = ffi::parse_id(client)?;
663		State::lock().client.get_mut(client)?.websocket.delay = Some(std::time::Duration::from_millis(delay_ms));
664		Ok(())
665	})
666}
667
668/// Enable or disable the WebSocket fallback entirely.
669///
670/// Enabled by default. Disabling it makes a UDP-blocked network fail outright rather
671/// than falling back, which is what you want when measuring the QUIC path.
672///
673/// Returns zero on success, or a negative code if the handle is unknown.
674#[unsafe(no_mangle)]
675pub extern "C" fn moq_client_set_websocket_enabled(client: u32, enabled: bool) -> i32 {
676	ffi::enter(move || {
677		let client = ffi::parse_id(client)?;
678		State::lock().client.get_mut(client)?.websocket.enabled = enabled;
679		Ok(())
680	})
681}
682
683/// Skip TLS certificate verification.
684///
685/// Development only: it accepts any certificate, so it defeats the point of TLS. Prefer
686/// [moq_client_set_tls_fingerprints] to trust one known self-signed certificate.
687///
688/// Returns zero on success, or a negative code if the handle is unknown.
689#[unsafe(no_mangle)]
690pub extern "C" fn moq_client_set_tls_disable_verify(client: u32, disable: bool) -> i32 {
691	ffi::enter(move || {
692		let client = ffi::parse_id(client)?;
693		State::lock().client.get_mut(client)?.tls.disable_verify = Some(disable);
694		Ok(())
695	})
696}
697
698/// Whether to also trust the platform's native root certificates.
699///
700/// By default the system roots are trusted only when no custom roots are configured.
701/// Set this to true to trust them alongside the roots from [moq_client_set_tls_roots],
702/// or false to trust only those.
703///
704/// Returns zero on success, or a negative code if the handle is unknown.
705#[unsafe(no_mangle)]
706pub extern "C" fn moq_client_set_tls_system_roots(client: u32, enabled: bool) -> i32 {
707	ffi::enter(move || {
708		let client = ffi::parse_id(client)?;
709		State::lock().client.get_mut(client)?.tls.system_roots = Some(enabled);
710		Ok(())
711	})
712}
713
714/// Trust these PEM root certificate files.
715///
716/// An empty list restores the default of using the platform's native root store.
717///
718/// Returns zero on success, or a negative code if the handle is unknown.
719///
720/// # Safety
721/// - The caller must ensure that `paths` is either NULL with a zero `count`, or a valid
722///   pointer to `count` [moq_string] values, each valid for its own length.
723#[unsafe(no_mangle)]
724pub unsafe extern "C" fn moq_client_set_tls_roots(client: u32, paths: *const moq_string, count: usize) -> i32 {
725	ffi::enter(move || {
726		let paths = unsafe { ffi::parse_strings(paths, count)? };
727		let client = ffi::parse_id(client)?;
728		State::lock().client.get_mut(client)?.tls.root = paths.into_iter().map(Into::into).collect();
729		Ok(())
730	})
731}
732
733/// Pin the peer to a certificate with one of these SHA-256 fingerprints, hex encoded.
734///
735/// The native equivalent of the browser's WebTransport `serverCertificateHashes`, taking
736/// the same values a relay reports for its self-signed certificate. Use it instead of
737/// [moq_client_set_tls_disable_verify] to trust one known certificate without accepting
738/// every certificate. An empty list clears any pinned fingerprints.
739///
740/// Returns zero on success, or a negative code if the handle is unknown.
741///
742/// # Safety
743/// - The caller must ensure that `fingerprints` is either NULL with a zero `count`, or a
744///   valid pointer to `count` [moq_string] values, each valid for its own length.
745#[unsafe(no_mangle)]
746pub unsafe extern "C" fn moq_client_set_tls_fingerprints(
747	client: u32,
748	fingerprints: *const moq_string,
749	count: usize,
750) -> i32 {
751	ffi::enter(move || {
752		let fingerprints = unsafe { ffi::parse_strings(fingerprints, count)? };
753		for fingerprint in &fingerprints {
754			moq_native::tls::parse_fingerprint(fingerprint).map_err(|err| Error::InvalidConfig(err.to_string()))?;
755		}
756		let client = ffi::parse_id(client)?;
757		State::lock().client.get_mut(client)?.tls.fingerprint = fingerprints;
758		Ok(())
759	})
760}
761
762/// Override the TLS server name (SNI) sent during the handshake.
763///
764/// Defaults to the host in the dial URL. Set this to reach a relay by IP while still
765/// validating its certificate against the name it was issued for. A NULL or empty value
766/// restores the default.
767///
768/// Returns zero on success, or a negative code if the handle is unknown.
769///
770/// # Safety
771/// - The caller must ensure that `name` is NULL or a valid pointer to `name_len` bytes.
772#[unsafe(no_mangle)]
773pub unsafe extern "C" fn moq_client_set_tls_host_name(client: u32, name: *const c_char, name_len: usize) -> i32 {
774	ffi::enter(move || {
775		let name = unsafe { ffi::parse_str_optional(name, name_len)? }.map(str::to_string);
776		let client = ffi::parse_id(client)?;
777		State::lock().client.get_mut(client)?.tls.host_name = name;
778		Ok(())
779	})
780}
781
782/// Present this PEM certificate chain when the relay requires mTLS.
783///
784/// Only certificates are read from the file; any private keys in it are ignored. Must be
785/// paired with [moq_client_set_tls_key] or the connect fails. A NULL or empty path clears it.
786///
787/// Returns zero on success, or a negative code if the handle is unknown.
788///
789/// # Safety
790/// - The caller must ensure that `path` is NULL or a valid pointer to `path_len` bytes.
791#[unsafe(no_mangle)]
792pub unsafe extern "C" fn moq_client_set_tls_cert(client: u32, path: *const c_char, path_len: usize) -> i32 {
793	ffi::enter(move || {
794		let path = unsafe { ffi::parse_str_optional(path, path_len)? }.map(Into::into);
795		let client = ffi::parse_id(client)?;
796		State::lock().client.get_mut(client)?.tls.cert = path;
797		Ok(())
798	})
799}
800
801/// Present this PEM private key when the relay requires mTLS.
802///
803/// Only the private key is read from the file; any certificates in it are ignored. Must
804/// be paired with [moq_client_set_tls_cert] or the connect fails. A NULL or empty path
805/// clears it.
806///
807/// Returns zero on success, or a negative code if the handle is unknown.
808///
809/// # Safety
810/// - The caller must ensure that `path` is NULL or a valid pointer to `path_len` bytes.
811#[unsafe(no_mangle)]
812pub unsafe extern "C" fn moq_client_set_tls_key(client: u32, path: *const c_char, path_len: usize) -> i32 {
813	ffi::enter(move || {
814		let path = unsafe { ffi::parse_str_optional(path, path_len)? }.map(Into::into);
815		let client = ffi::parse_id(client)?;
816		State::lock().client.get_mut(client)?.tls.key = path;
817		Ok(())
818	})
819}
820
821/// Set the delay before the first reconnect attempt, in milliseconds.
822///
823/// The delay grows from here by the multiplier after each failure. Defaults to 1s.
824///
825/// Returns zero on success, or a negative code if the handle is unknown.
826#[unsafe(no_mangle)]
827pub extern "C" fn moq_client_set_backoff_initial(client: u32, delay_ms: u64) -> i32 {
828	ffi::enter(move || {
829		let client = ffi::parse_id(client)?;
830		State::lock().client.get_mut(client)?.backoff.initial = std::time::Duration::from_millis(delay_ms);
831		Ok(())
832	})
833}
834
835/// Set the multiplier applied to the reconnect delay after each failed attempt.
836///
837/// Defaults to 2. A multiplier of 1 keeps the delay flat.
838///
839/// Returns zero on success, or a negative code if the handle is unknown.
840#[unsafe(no_mangle)]
841pub extern "C" fn moq_client_set_backoff_multiplier(client: u32, multiplier: u32) -> i32 {
842	ffi::enter(move || {
843		let client = ffi::parse_id(client)?;
844		State::lock().client.get_mut(client)?.backoff.multiplier = multiplier;
845		Ok(())
846	})
847}
848
849/// Set the ceiling on the growing reconnect delay, in milliseconds.
850///
851/// Defaults to 5s.
852///
853/// Returns zero on success, or a negative code if the handle is unknown.
854#[unsafe(no_mangle)]
855pub extern "C" fn moq_client_set_backoff_max(client: u32, delay_ms: u64) -> i32 {
856	ffi::enter(move || {
857		let client = ffi::parse_id(client)?;
858		State::lock().client.get_mut(client)?.backoff.max = std::time::Duration::from_millis(delay_ms);
859		Ok(())
860	})
861}
862
863/// Set how long to keep retrying before giving up, in milliseconds.
864///
865/// Zero retries forever. Defaults to 10s. This is also how long published
866/// broadcasts linger across a drop, so a longer timeout papers over a longer relay
867/// outage.
868///
869/// Returns zero on success, or a negative code if the handle is unknown.
870#[unsafe(no_mangle)]
871pub extern "C" fn moq_client_set_backoff_timeout(client: u32, timeout_ms: u64) -> i32 {
872	ffi::enter(move || {
873		let client = ffi::parse_id(client)?;
874		State::lock().client.get_mut(client)?.backoff.timeout = std::time::Duration::from_millis(timeout_ms);
875		Ok(())
876	})
877}
878
879/// Set the maximum concurrent QUIC streams per connection, bidirectional and
880/// unidirectional alike.
881///
882/// Defaults to 1024. MoQ opens a stream per group, so a busy publisher wants this high.
883/// QUIC only; the WebSocket fallback ignores it.
884///
885/// Returns zero on success, or a negative code if the handle is unknown.
886#[unsafe(no_mangle)]
887pub extern "C" fn moq_client_set_quic_max_streams(client: u32, max_streams: u64) -> i32 {
888	ffi::enter(move || {
889		let client = ffi::parse_id(client)?;
890		State::lock().client.get_mut(client)?.quic.max_streams = Some(max_streams);
891		Ok(())
892	})
893}
894
895/// Set the idle timeout before an inactive connection is dropped, in milliseconds.
896///
897/// Defaults to 30s. QUIC carries this as a millisecond varint, so a value of 2^62 or
898/// more is rejected when the connection is dialed. QUIC only.
899///
900/// Returns zero on success, or a negative code if the handle is unknown.
901#[unsafe(no_mangle)]
902pub extern "C" fn moq_client_set_quic_idle_timeout(client: u32, timeout_ms: u64) -> i32 {
903	ffi::enter(move || {
904		let client = ffi::parse_id(client)?;
905		State::lock().client.get_mut(client)?.quic.idle_timeout = Some(std::time::Duration::from_millis(timeout_ms));
906		Ok(())
907	})
908}
909
910/// Set the keep-alive ping interval, in milliseconds.
911///
912/// Defaults to 5s; zero disables the pings. QUIC only.
913///
914/// Returns zero on success, or a negative code if the handle is unknown.
915#[unsafe(no_mangle)]
916pub extern "C" fn moq_client_set_quic_keep_alive(client: u32, interval_ms: u64) -> i32 {
917	ffi::enter(move || {
918		let client = ffi::parse_id(client)?;
919		State::lock().client.get_mut(client)?.quic.keep_alive = Some(std::time::Duration::from_millis(interval_ms));
920		Ok(())
921	})
922}
923
924/// Enable or disable UDP generic segmentation offload.
925///
926/// GSO batches sends into one syscall for throughput, and defaults to on. Some NICs and
927/// middleboxes mangle segmented packets, so turn it off if large sends vanish. QUIC only.
928///
929/// Returns zero on success, or a negative code if the handle is unknown.
930#[unsafe(no_mangle)]
931pub extern "C" fn moq_client_set_quic_gso(client: u32, enabled: bool) -> i32 {
932	ffi::enter(move || {
933		let client = ffi::parse_id(client)?;
934		State::lock().client.get_mut(client)?.quic.gso = Some(enabled);
935		Ok(())
936	})
937}
938
939/// Enable or disable path MTU discovery.
940///
941/// Defaults to off. QUIC only.
942///
943/// Returns zero on success, or a negative code if the handle is unknown.
944#[unsafe(no_mangle)]
945pub extern "C" fn moq_client_set_quic_mtu_discovery(client: u32, enabled: bool) -> i32 {
946	ffi::enter(move || {
947		let client = ffi::parse_id(client)?;
948		State::lock().client.get_mut(client)?.quic.mtu_discovery = Some(enabled);
949		Ok(())
950	})
951}
952
953/// Set the congestion control family.
954///
955/// Either `"loss"` (CUBIC, throughput-oriented) or `"delay"` (BBR, which keeps queues
956/// short and the send rate steady enough for an encoder to track). A NULL or empty value
957/// puts it back to the backend's own default. QUIC only.
958///
959/// Returns zero on success, or a negative code if the handle is unknown or the family is
960/// unrecognized.
961///
962/// # Safety
963/// - The caller must ensure that `family` is either NULL or valid for `family_len` bytes.
964#[unsafe(no_mangle)]
965pub unsafe extern "C" fn moq_client_set_quic_congestion_control(
966	client: u32,
967	family: *const c_char,
968	family_len: usize,
969) -> i32 {
970	ffi::enter(move || {
971		// Parse before taking the lock, so a bad value leaves the config untouched.
972		let family = match unsafe { ffi::parse_str_optional(family, family_len)? } {
973			Some(value) => Some(moq_native::quic::CongestionControl::from_str(value).map_err(Error::InvalidConfig)?),
974			None => None,
975		};
976
977		let client = ffi::parse_id(client)?;
978		State::lock().client.get_mut(client)?.quic.congestion_control = family;
979		Ok(())
980	})
981}
982
983/// Set the directory to write qlog traces into.
984///
985/// A NULL or empty value disables them. Dialing errors if this build has no qlog
986/// support. QUIC only.
987///
988/// Returns zero on success, or a negative code if the handle is unknown.
989///
990/// # Safety
991/// - The caller must ensure that `dir` is either NULL or valid for `dir_len` bytes.
992#[unsafe(no_mangle)]
993pub unsafe extern "C" fn moq_client_set_quic_qlog(client: u32, dir: *const c_char, dir_len: usize) -> i32 {
994	ffi::enter(move || {
995		let dir = unsafe { ffi::parse_str_optional(dir, dir_len)? }.map(Into::into);
996		let client = ffi::parse_id(client)?;
997		State::lock().client.get_mut(client)?.quic.qlog = dir;
998		Ok(())
999	})
1000}
1001
1002/// Read the connect timeout, in milliseconds. See [moq_client_set_connect_timeout].
1003///
1004/// A knob never set reads back as its default, so a fresh [moq_client_create] handle
1005/// reports the defaults a dial would use. That is what a settings UI should show,
1006/// rather than repeating numbers that go stale when a default is retuned.
1007///
1008/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1009///
1010/// # Safety
1011/// - The caller must ensure that `out` points to a writable `uint64_t`.
1012#[unsafe(no_mangle)]
1013pub unsafe extern "C" fn moq_client_get_connect_timeout(client: u32, out: *mut u64) -> i32 {
1014	ffi::enter(move || {
1015		let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1016		let client = ffi::parse_id(client)?;
1017		*out = millis(State::lock().client.get_mut(client)?.resolved_connect_timeout());
1018		Ok(())
1019	})
1020}
1021
1022/// Read the Happy Eyeballs stagger, in milliseconds. See [moq_client_set_failover_delay]
1023/// and [moq_client_get_connect_timeout] for what an unset knob reports.
1024///
1025/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1026///
1027/// # Safety
1028/// - The caller must ensure that `out` points to a writable `uint64_t`.
1029#[unsafe(no_mangle)]
1030pub unsafe extern "C" fn moq_client_get_failover_delay(client: u32, out: *mut u64) -> i32 {
1031	ffi::enter(move || {
1032		let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1033		let client = ffi::parse_id(client)?;
1034		*out = millis(State::lock().client.get_mut(client)?.resolved_failover_delay());
1035		Ok(())
1036	})
1037}
1038
1039/// Read the Resolution Delay, in milliseconds. See [moq_client_set_resolution_delay]
1040/// and [moq_client_get_connect_timeout] for what an unset knob reports.
1041///
1042/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1043///
1044/// # Safety
1045/// - The caller must ensure that `out` points to a writable `uint64_t`.
1046#[unsafe(no_mangle)]
1047pub unsafe extern "C" fn moq_client_get_resolution_delay(client: u32, out: *mut u64) -> i32 {
1048	ffi::enter(move || {
1049		let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1050		let client = ffi::parse_id(client)?;
1051		*out = millis(State::lock().client.get_mut(client)?.resolved_resolution_delay());
1052		Ok(())
1053	})
1054}
1055
1056/// Read the first reconnect delay, in milliseconds. See [moq_client_set_backoff_initial].
1057///
1058/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1059///
1060/// # Safety
1061/// - The caller must ensure that `out` points to a writable `uint64_t`.
1062#[unsafe(no_mangle)]
1063pub unsafe extern "C" fn moq_client_get_backoff_initial(client: u32, out: *mut u64) -> i32 {
1064	ffi::enter(move || {
1065		let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1066		let client = ffi::parse_id(client)?;
1067		*out = millis(State::lock().client.get_mut(client)?.backoff.initial);
1068		Ok(())
1069	})
1070}
1071
1072/// Read the reconnect delay multiplier. See [moq_client_set_backoff_multiplier].
1073///
1074/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1075///
1076/// # Safety
1077/// - The caller must ensure that `out` points to a writable `uint32_t`.
1078#[unsafe(no_mangle)]
1079pub unsafe extern "C" fn moq_client_get_backoff_multiplier(client: u32, out: *mut u32) -> i32 {
1080	ffi::enter(move || {
1081		let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1082		let client = ffi::parse_id(client)?;
1083		*out = State::lock().client.get_mut(client)?.backoff.multiplier;
1084		Ok(())
1085	})
1086}
1087
1088/// Read the reconnect delay ceiling, in milliseconds. See [moq_client_set_backoff_max].
1089///
1090/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1091///
1092/// # Safety
1093/// - The caller must ensure that `out` points to a writable `uint64_t`.
1094#[unsafe(no_mangle)]
1095pub unsafe extern "C" fn moq_client_get_backoff_max(client: u32, out: *mut u64) -> i32 {
1096	ffi::enter(move || {
1097		let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1098		let client = ffi::parse_id(client)?;
1099		*out = millis(State::lock().client.get_mut(client)?.backoff.max);
1100		Ok(())
1101	})
1102}
1103
1104/// Read how long reconnecting keeps trying, in milliseconds. Zero means forever. See
1105/// [moq_client_set_backoff_timeout].
1106///
1107/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1108///
1109/// # Safety
1110/// - The caller must ensure that `out` points to a writable `uint64_t`.
1111#[unsafe(no_mangle)]
1112pub unsafe extern "C" fn moq_client_get_backoff_timeout(client: u32, out: *mut u64) -> i32 {
1113	ffi::enter(move || {
1114		let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1115		let client = ffi::parse_id(client)?;
1116		*out = millis(State::lock().client.get_mut(client)?.backoff.timeout);
1117		Ok(())
1118	})
1119}
1120
1121/// Read the maximum concurrent QUIC streams. See [moq_client_set_quic_max_streams].
1122///
1123/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1124///
1125/// # Safety
1126/// - The caller must ensure that `out` points to a writable `uint64_t`.
1127#[unsafe(no_mangle)]
1128pub unsafe extern "C" fn moq_client_get_quic_max_streams(client: u32, out: *mut u64) -> i32 {
1129	ffi::enter(move || {
1130		let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1131		let client = ffi::parse_id(client)?;
1132		*out = State::lock().client.get_mut(client)?.quic.resolve().max_streams;
1133		Ok(())
1134	})
1135}
1136
1137/// Read the QUIC idle timeout, in milliseconds. See [moq_client_set_quic_idle_timeout].
1138///
1139/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1140///
1141/// # Safety
1142/// - The caller must ensure that `out` points to a writable `uint64_t`.
1143#[unsafe(no_mangle)]
1144pub unsafe extern "C" fn moq_client_get_quic_idle_timeout(client: u32, out: *mut u64) -> i32 {
1145	ffi::enter(move || {
1146		let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1147		let client = ffi::parse_id(client)?;
1148		*out = millis(State::lock().client.get_mut(client)?.quic.resolve().idle_timeout);
1149		Ok(())
1150	})
1151}
1152
1153/// Read the QUIC keep-alive interval, in milliseconds. Zero means the pings are
1154/// disabled. See [moq_client_set_quic_keep_alive].
1155///
1156/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1157///
1158/// # Safety
1159/// - The caller must ensure that `out` points to a writable `uint64_t`.
1160#[unsafe(no_mangle)]
1161pub unsafe extern "C" fn moq_client_get_quic_keep_alive(client: u32, out: *mut u64) -> i32 {
1162	ffi::enter(move || {
1163		let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1164		let client = ffi::parse_id(client)?;
1165		let keep_alive = State::lock().client.get_mut(client)?.quic.resolve().keep_alive;
1166		*out = keep_alive.map(millis).unwrap_or(0);
1167		Ok(())
1168	})
1169}
1170
1171/// Read whether the WebSocket fallback races the QUIC attempt. See
1172/// [moq_client_set_websocket_enabled].
1173///
1174/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1175///
1176/// # Safety
1177/// - The caller must ensure that `out` points to a writable `bool`.
1178#[unsafe(no_mangle)]
1179pub unsafe extern "C" fn moq_client_get_websocket_enabled(client: u32, out: *mut bool) -> i32 {
1180	ffi::enter(move || {
1181		let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1182		let client = ffi::parse_id(client)?;
1183		*out = State::lock().client.get_mut(client)?.websocket.enabled;
1184		Ok(())
1185	})
1186}
1187
1188/// Read the WebSocket fallback delay, in milliseconds. See
1189/// [moq_client_set_websocket_delay].
1190///
1191/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1192///
1193/// # Safety
1194/// - The caller must ensure that `out` points to a writable `uint64_t`.
1195#[unsafe(no_mangle)]
1196pub unsafe extern "C" fn moq_client_get_websocket_delay(client: u32, out: *mut u64) -> i32 {
1197	ffi::enter(move || {
1198		let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1199		let client = ffi::parse_id(client)?;
1200		let delay = State::lock().client.get_mut(client)?.websocket.delay;
1201		*out = delay.map(millis).unwrap_or(0);
1202		Ok(())
1203	})
1204}
1205
1206/// Start establishing a connection to a MoQ server using a client configuration.
1207///
1208/// Identical to [moq_session_connect] but dials with the settings on `client` (created
1209/// by [moq_client_create]) instead of the defaults. The config is cloned, so the handle
1210/// stays reusable and editable afterwards. A `client` of 0 means the defaults, which is
1211/// exactly what [moq_session_connect] does.
1212///
1213/// Returns a non-zero session handle on success, or a negative code on (immediate)
1214/// failure. Close it with [moq_session_close]. See [moq_session_connect] for the
1215/// `on_status` contract, which is the same here.
1216///
1217/// # Safety
1218/// - The caller must ensure that url is a valid pointer to url_len bytes of data.
1219/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_status` callback.
1220#[unsafe(no_mangle)]
1221pub unsafe extern "C" fn moq_client_connect(
1222	url: *const c_char,
1223	url_len: usize,
1224	client: u32,
1225	origin_publish: u32,
1226	origin_consume: u32,
1227	on_status: Option<extern "C" fn(user_data: *mut c_void, code: i32)>,
1228	user_data: *mut c_void,
1229) -> i32 {
1230	ffi::enter(move || unsafe {
1231		connect_session(
1232			url,
1233			url_len,
1234			client,
1235			origin_publish,
1236			origin_consume,
1237			on_status,
1238			user_data,
1239		)
1240	})
1241}
1242
1243/// Resolve handles under the global lock, prepare the client without it, then insert
1244/// the ready session under a short second lock.
1245unsafe fn connect_session(
1246	url: *const c_char,
1247	url_len: usize,
1248	client: u32,
1249	origin_publish: u32,
1250	origin_consume: u32,
1251	on_status: Option<extern "C" fn(user_data: *mut c_void, code: i32)>,
1252	user_data: *mut c_void,
1253) -> Result<crate::Id, Error> {
1254	let url = ffi::parse_url(url, url_len)?;
1255	let client = ffi::parse_id_optional(client)?;
1256	let origin_publish = ffi::parse_id_optional(origin_publish)?;
1257	let origin_consume = ffi::parse_id_optional(origin_consume)?;
1258
1259	let (config, publish, consume) = {
1260		let state = State::lock();
1261		let config = state.client.config(client)?;
1262		let publish = origin_publish.map(|id| state.origin.get(id)).transpose()?.cloned();
1263		let consume = origin_consume.map(|id| state.origin.get(id)).transpose()?.cloned();
1264		(config, publish, consume)
1265	};
1266
1267	let callback = unsafe { ffi::OnStatus::new(user_data, on_status) };
1268	let request = Connect {
1269		config,
1270		url,
1271		publish,
1272		consume,
1273		callback,
1274	}
1275	.prepare()?;
1276
1277	State::lock().session.connect(request)
1278}
1279
1280/// Start establishing a connection to a MoQ server.
1281///
1282/// Takes origin handles, which are used for publishing and consuming broadcasts respectively.
1283/// - Any broadcasts in `origin_publish` will be announced to the server.
1284/// - Any broadcasts announced by the server will be available in `origin_consume`.
1285/// - If an origin handle is 0, that functionality is completely disabled.
1286///
1287/// This may be called multiple times to connect to different servers.
1288/// Origins can be shared across sessions, useful for fanout or relaying.
1289///
1290/// Dials with the default settings. Use [moq_client_connect] to pin a protocol version,
1291/// adjust TLS trust, or tune the transport.
1292///
1293/// Returns a non-zero handle to the session on success, or a negative code on (immediate) failure.
1294/// You should call [moq_session_close], even on error, to free up resources.
1295///
1296/// The session reconnects automatically with exponential backoff if the connection drops.
1297/// Published broadcasts are re-announced and consumers re-subscribed on each reconnect,
1298/// since the origins outlive the underlying connection.
1299///
1300/// `on_status` reports the session lifecycle through its status code:
1301/// - `> 0` on every (re)connect, carrying the connection epoch (`1` = first connect,
1302///   `2` = first reconnect, and so on), so a reconnect is distinguishable from the
1303///   initial connect. May fire repeatedly. Transient disconnects are not reported.
1304/// - `0` when the session is closed cleanly via [moq_session_close] (terminal).
1305/// - a negative error code if reconnection permanently gives up, e.g. the backoff
1306///   timeout is exceeded (terminal).
1307///
1308/// After a terminal (`<= 0`) status, `on_status` is never called again and `user_data`
1309/// is never touched again, so that final callback is the point to release `user_data`.
1310/// The terminal `0` fires even after [moq_session_close], so do not free `user_data` on
1311/// the close call itself.
1312///
1313/// # Safety
1314/// - The caller must ensure that url is a valid pointer to url_len bytes of data.
1315/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_status` callback.
1316#[unsafe(no_mangle)]
1317pub unsafe extern "C" fn moq_session_connect(
1318	url: *const c_char,
1319	url_len: usize,
1320	origin_publish: u32,
1321	origin_consume: u32,
1322	on_status: Option<extern "C" fn(user_data: *mut c_void, code: i32)>,
1323	user_data: *mut c_void,
1324) -> i32 {
1325	ffi::enter(move || unsafe {
1326		connect_session(url, url_len, 0, origin_publish, origin_consume, on_status, user_data)
1327	})
1328}
1329
1330/// Request that a session shut down.
1331///
1332/// Returns immediately: zero on success, or a negative code if the session is
1333/// unknown or already closing. Does NOT free `user_data`. The
1334/// [moq_session_connect] `on_status` callback still fires once more with a
1335/// terminal `0` (or a negative error), and that final callback is where
1336/// `user_data` should be released. Safe to call from any thread, including from
1337/// within `on_status`.
1338#[unsafe(no_mangle)]
1339pub extern "C" fn moq_session_close(session: u32) -> i32 {
1340	ffi::enter(move || {
1341		let session = ffi::parse_id(session)?;
1342		State::lock().session.close(session)
1343	})
1344}
1345
1346/// Snapshot the current connection statistics for a session.
1347///
1348/// Fills `dst` with a point-in-time view of the underlying QUIC/WebTransport connection
1349/// (RTT, bandwidth estimates, byte/packet counters). Each metric carries a `*_valid` flag
1350/// since availability depends on the transport backend; see [moq_connection_stats].
1351///
1352/// Returns zero on success, or a negative code on failure: the session handle is unknown, or
1353/// the session is currently reconnecting and has no live connection (in which case `dst` is
1354/// left untouched). Safe to call repeatedly to poll stats over the life of the session.
1355///
1356/// # Safety
1357/// - The caller must ensure that `dst` is a valid pointer to a [moq_connection_stats] struct.
1358#[unsafe(no_mangle)]
1359pub unsafe extern "C" fn moq_session_stats(session: u32, dst: *mut moq_connection_stats) -> i32 {
1360	ffi::enter(move || {
1361		let session = ffi::parse_id(session)?;
1362		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1363		let stats = State::lock().session.stats(session)?;
1364		*dst = moq_connection_stats::from(&stats);
1365		Ok(())
1366	})
1367}
1368
1369/// Create an origin for publishing broadcasts.
1370///
1371/// Origins contain any number of broadcasts addressed by path.
1372/// The same broadcast can be published to multiple origins under different paths.
1373///
1374/// [moq_origin_announced] can be used to discover broadcasts published to this origin.
1375/// This is extremely useful for discovering what is available on the server to [moq_origin_request].
1376///
1377/// Returns a non-zero handle to the origin on success.
1378#[unsafe(no_mangle)]
1379pub extern "C" fn moq_origin_create() -> i32 {
1380	ffi::enter(move || State::lock().origin.create())
1381}
1382
1383/// Create a broadcast at `path` on an origin, for publishing media tracks.
1384///
1385/// The broadcast starts live: the origin announces the path so consumers can discover it,
1386/// becoming visible shortly after this returns. Fill it with the `moq_publish_*` functions.
1387/// Toggle discoverability with [moq_publish_set_announce]; [moq_publish_finish] unpublishes
1388/// immediately.
1389///
1390/// Returns a non-zero broadcast handle on success, or a negative code on failure.
1391///
1392/// # Safety
1393/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
1394#[unsafe(no_mangle)]
1395pub unsafe extern "C" fn moq_origin_publish(origin: u32, path: *const c_char, path_len: usize) -> i32 {
1396	ffi::enter(move || {
1397		let origin = ffi::parse_id(origin)?;
1398		let path = unsafe { ffi::parse_str(path, path_len)? };
1399
1400		let mut state = State::lock();
1401		let broadcast = state.origin.publish(origin, path)?;
1402		state.publish.create(broadcast)
1403	})
1404}
1405
1406/// Learn about all broadcasts published to an origin.
1407///
1408/// `on_announce` is invoked with a positive announced ID for each broadcast,
1409/// then exactly once more with a terminal code: `0` (stopped cleanly) or a
1410/// negative error. After the terminal (`<= 0`) callback, `on_announce` is never
1411/// called again and `user_data` is never touched again, so release `user_data`
1412/// there. The terminal callback fires even after [moq_origin_announced_close].
1413///
1414/// - [moq_origin_announced_info] is used to query information about the broadcast.
1415/// - [moq_origin_announced_free] releases each delivered announced ID once read.
1416/// - [moq_origin_announced_close] is used to stop receiving announcements.
1417///
1418/// Returns a non-zero handle on success, or a negative code on failure.
1419///
1420/// # Safety
1421/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_announce` callback.
1422#[unsafe(no_mangle)]
1423pub unsafe extern "C" fn moq_origin_announced(
1424	origin: u32,
1425	on_announce: Option<extern "C" fn(user_data: *mut c_void, announced: i32)>,
1426	user_data: *mut c_void,
1427) -> i32 {
1428	ffi::enter(move || {
1429		let origin = ffi::parse_id(origin)?;
1430		let on_announce = unsafe { ffi::OnStatus::new(user_data, on_announce) };
1431		State::lock().origin.announced(origin, on_announce)
1432	})
1433}
1434
1435/// Query information about a broadcast discovered by [moq_origin_announced].
1436///
1437/// The destination is filled with the broadcast information. The `path` pointer borrows
1438/// the announcement's storage: copy it out before calling [moq_origin_announced_free], which
1439/// invalidates it.
1440///
1441/// Returns a zero on success, or a negative code on failure.
1442///
1443/// # Safety
1444/// - The caller must ensure that `dst` is a valid pointer to a [moq_announced] struct.
1445#[unsafe(no_mangle)]
1446pub unsafe extern "C" fn moq_origin_announced_info(announced: u32, dst: *mut moq_announced) -> i32 {
1447	ffi::enter(move || {
1448		let announced = ffi::parse_id(announced)?;
1449		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1450		State::lock().origin.announced_info(announced, dst)
1451	})
1452}
1453
1454/// Free a single announcement delivered to a [moq_origin_announced] `on_announce` callback.
1455///
1456/// Each announce / unannounce event hands the callback a distinct announcement handle (read
1457/// with [moq_origin_announced_info]); release it here once done to avoid leaking one per event
1458/// over the life of the listener. This is per-announcement and distinct from
1459/// [moq_origin_announced_close], which stops the listener itself. After freeing, any `path`
1460/// pointer obtained from [moq_origin_announced_info] for this handle is dangling.
1461///
1462/// Returns zero on success, or a negative code if the handle is unknown.
1463#[unsafe(no_mangle)]
1464pub extern "C" fn moq_origin_announced_free(announced: u32) -> i32 {
1465	ffi::enter(move || {
1466		let announced = ffi::parse_id(announced)?;
1467		State::lock().origin.announced_free(announced)
1468	})
1469}
1470
1471/// Stop receiving announcements for broadcasts published to an origin.
1472///
1473/// Returns immediately: zero on success, or a negative code if already closed.
1474/// Does NOT free `user_data`. The [moq_origin_announced] `on_announce` callback
1475/// still fires once more with a terminal `0` (or a negative error), and that
1476/// final callback is where `user_data` should be released.
1477#[unsafe(no_mangle)]
1478pub extern "C" fn moq_origin_announced_close(announced: u32) -> i32 {
1479	ffi::enter(move || {
1480		let announced = ffi::parse_id(announced)?;
1481		State::lock().origin.announced_close(announced)
1482	})
1483}
1484
1485/// Consume a broadcast from an origin by path, waiting until it is announced.
1486///
1487/// Resolves against future announcements: it waits for the announcement to arrive (e.g. over the
1488/// network) and then delivers the broadcast handle via `on_broadcast`. Use it right after
1489/// [moq_session_connect] to avoid racing announcement gossip. To resolve against only what is
1490/// announced now (plus any dynamic fallback), use [moq_origin_request] instead.
1491///
1492/// `on_broadcast` is invoked with a positive broadcast handle once announced, then exactly once
1493/// more with a terminal code: `0` (the wait finished, including after
1494/// [moq_origin_consume_announced_close]) or a negative error. After the terminal (`<= 0`) callback,
1495/// `on_broadcast` is never called again and `user_data` is never touched again, so release
1496/// `user_data` there. The broadcast handle is usable with [moq_consume_catalog] / [moq_consume_track]
1497/// and must be freed separately with [moq_consume_close].
1498///
1499/// Returns a non-zero handle to the wait on success, or a negative code on (immediate) failure.
1500///
1501/// # Safety
1502/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
1503/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_broadcast` callback.
1504#[unsafe(no_mangle)]
1505pub unsafe extern "C" fn moq_origin_consume_announced(
1506	origin: u32,
1507	path: *const c_char,
1508	path_len: usize,
1509	on_broadcast: Option<extern "C" fn(user_data: *mut c_void, broadcast: i32)>,
1510	user_data: *mut c_void,
1511) -> i32 {
1512	ffi::enter(move || {
1513		let origin = ffi::parse_id(origin)?;
1514		let path = unsafe { ffi::parse_str(path, path_len)? }.to_string();
1515		let on_broadcast = unsafe { ffi::OnStatus::new(user_data, on_broadcast) };
1516		State::lock().origin.consume_announced(origin, path, on_broadcast)
1517	})
1518}
1519
1520/// Abort a wait started by [moq_origin_consume_announced].
1521///
1522/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
1523/// `user_data`. The [moq_origin_consume_announced] `on_broadcast` callback still fires once more
1524/// with a terminal `0` (or a negative error), and that final callback is where `user_data` should
1525/// be released. Any broadcast handle already delivered is unaffected and must still be freed with
1526/// [moq_consume_close].
1527#[unsafe(no_mangle)]
1528pub extern "C" fn moq_origin_consume_announced_close(task: u32) -> i32 {
1529	ffi::enter(move || {
1530		let task = ffi::parse_id(task)?;
1531		State::lock().origin.consume_announced_close(task)
1532	})
1533}
1534
1535/// Request a broadcast from an origin by path, resolving as soon as it can be served.
1536///
1537/// Resolves against what is announced *now* plus any dynamic fallback, where
1538/// [moq_origin_consume_announced] waits indefinitely for a future announcement: it returns an
1539/// already-announced broadcast at once, otherwise falls back to a dynamic handler on the origin
1540/// (if any), and fails when neither can serve the path. It does NOT wait for a later
1541/// announcement.
1542///
1543/// `on_broadcast` is invoked with a positive broadcast handle once served, then exactly once more
1544/// with a terminal code: `0` (finished, including after [moq_origin_request_close]) or a negative
1545/// error. After the terminal (`<= 0`) callback, `user_data` is never touched again, so release it
1546/// there. The broadcast handle is usable with [moq_consume_catalog] / [moq_consume_track] and must
1547/// be freed separately with [moq_consume_close].
1548///
1549/// Returns a non-zero handle to the request on success, or a negative code on (immediate) failure.
1550///
1551/// # Safety
1552/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
1553/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_broadcast` callback.
1554#[unsafe(no_mangle)]
1555pub unsafe extern "C" fn moq_origin_request(
1556	origin: u32,
1557	path: *const c_char,
1558	path_len: usize,
1559	on_broadcast: Option<extern "C" fn(user_data: *mut c_void, broadcast: i32)>,
1560	user_data: *mut c_void,
1561) -> i32 {
1562	ffi::enter(move || {
1563		let origin = ffi::parse_id(origin)?;
1564		let path = unsafe { ffi::parse_str(path, path_len)? }.to_string();
1565		let on_broadcast = unsafe { ffi::OnStatus::new(user_data, on_broadcast) };
1566		State::lock().origin.request(origin, path, on_broadcast)
1567	})
1568}
1569
1570/// Abort a request started by [moq_origin_request].
1571///
1572/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
1573/// `user_data`; the [moq_origin_request] `on_broadcast` callback fires once more with a terminal
1574/// code, which is where `user_data` should be released. Any broadcast handle already delivered is
1575/// unaffected and must still be freed with [moq_consume_close].
1576#[unsafe(no_mangle)]
1577pub extern "C" fn moq_origin_request_close(task: u32) -> i32 {
1578	ffi::enter(move || {
1579		let task = ffi::parse_id(task)?;
1580		State::lock().origin.consume_announced_close(task)
1581	})
1582}
1583
1584/// Close an origin and clean up its resources.
1585///
1586/// Returns a zero on success, or a negative code on failure.
1587#[unsafe(no_mangle)]
1588pub extern "C" fn moq_origin_close(origin: u32) -> i32 {
1589	ffi::enter(move || {
1590		let origin = ffi::parse_id(origin)?;
1591		State::lock().origin.close(origin)
1592	})
1593}
1594
1595/// Set whether a broadcast created by [moq_origin_publish] is live: announced by its origin.
1596///
1597/// A non-live broadcast stays reachable by exact path for subscribes and fetches; it just is
1598/// not announced. This is how a publisher goes on and off the air without tearing down the
1599/// broadcast.
1600///
1601/// Returns a zero on success, or a negative code on failure.
1602#[unsafe(no_mangle)]
1603pub extern "C" fn moq_publish_set_announce(broadcast: u32, announce: bool) -> i32 {
1604	ffi::enter(move || {
1605		let broadcast = ffi::parse_id(broadcast)?;
1606		State::lock().publish.set_announce(broadcast, announce)
1607	})
1608}
1609
1610/// Finish a broadcast and release it, ending its catalog cleanly.
1611///
1612/// Subscribers see a normal end of stream rather than an error, and the origin unpublishes
1613/// the path immediately.
1614///
1615/// Returns a zero on success, or a negative code on failure.
1616#[unsafe(no_mangle)]
1617pub extern "C" fn moq_publish_finish(broadcast: u32) -> i32 {
1618	ffi::enter(move || {
1619		let broadcast = ffi::parse_id(broadcast)?;
1620		State::lock().publish.finish(broadcast)
1621	})
1622}
1623
1624/// Create a new media track for a broadcast
1625///
1626/// All frames in [moq_publish_media_frame] must be written in decode order.
1627/// The `format` controls the encoding, both of `init` and frame payloads.
1628///
1629/// Returns a non-zero handle to the track on success, or a negative code on failure.
1630///
1631/// # Safety
1632/// - The caller must ensure that format is a valid pointer to format_len bytes of data.
1633/// - The caller must ensure that init is a valid pointer to init_size bytes of data.
1634#[unsafe(no_mangle)]
1635pub unsafe extern "C" fn moq_publish_media(
1636	broadcast: u32,
1637	format: *const c_char,
1638	format_len: usize,
1639	init: *const u8,
1640	init_size: usize,
1641) -> i32 {
1642	ffi::enter(move || {
1643		let broadcast = ffi::parse_id(broadcast)?;
1644		let format = unsafe { ffi::parse_str(format, format_len)? };
1645		let init = unsafe { ffi::parse_slice(init, init_size)? };
1646
1647		State::lock().publish.media(broadcast, format, init)
1648	})
1649}
1650
1651/// Finish a media track, flushing any buffered frames. No more frames can be written.
1652///
1653/// Returns a zero on success, or a negative code on failure.
1654#[unsafe(no_mangle)]
1655pub extern "C" fn moq_publish_media_finish(export: u32) -> i32 {
1656	ffi::enter(move || {
1657		let export = ffi::parse_id(export)?;
1658		State::lock().publish.media_finish(export)
1659	})
1660}
1661
1662/// Write data to a track.
1663///
1664/// The encoding of `data` depends on the track `format`.
1665/// The timestamp is in microseconds.
1666///
1667/// Returns a zero on success, or a negative code on failure.
1668///
1669/// # Safety
1670/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
1671#[unsafe(no_mangle)]
1672pub unsafe extern "C" fn moq_publish_media_frame(
1673	media: u32,
1674	payload: *const u8,
1675	payload_size: usize,
1676	timestamp_us: u64,
1677) -> i32 {
1678	ffi::enter(move || {
1679		let media = ffi::parse_id(media)?;
1680		let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
1681		let timestamp = hang::container::Timestamp::from_micros(timestamp_us)?;
1682		State::lock().publish.media_frame(media, payload, timestamp)
1683	})
1684}
1685
1686/// Replace the catalog properties shared by every video rendition.
1687///
1688/// Rotation is clockwise and normalized to the nearest quarter turn. A field whose matching `has_*` flag is false is removed from the next catalog update.
1689///
1690/// Returns zero on success, or a negative code on failure.
1691///
1692/// # Safety
1693/// - The caller must ensure that `properties` points to a valid [moq_video_properties].
1694#[unsafe(no_mangle)]
1695pub unsafe extern "C" fn moq_publish_video_properties(broadcast: u32, properties: *const moq_video_properties) -> i32 {
1696	ffi::enter(move || {
1697		let broadcast = ffi::parse_id(broadcast)?;
1698		let properties = unsafe { properties.as_ref() }.ok_or(Error::InvalidPointer)?;
1699
1700		let mut value = hang::catalog::VideoProperties::default();
1701		value.display = properties.has_display.then_some(hang::catalog::Display {
1702			width: properties.display_width,
1703			height: properties.display_height,
1704		});
1705		value.rotation = properties.has_rotation.then_some(properties.rotation);
1706		value.flip = properties.has_flip.then_some(properties.flip);
1707
1708		State::lock().publish.video_properties(broadcast, value)
1709	})
1710}
1711
1712/// Add or replace a video rendition in a broadcast's catalog.
1713///
1714/// This is the producer counterpart to [moq_consume_video_config]: instead of
1715/// reading a rendition out of a catalog, it writes one into the catalog of a
1716/// broadcast created with [moq_origin_publish]. The rendition is keyed by
1717/// `config.name`; calling this again with the same name replaces it. The
1718/// updated catalog is published to subscribers automatically.
1719///
1720/// The struct fields are read as inputs:
1721/// - `name` / `codec` are required (NOT NULL terminated) string slices.
1722/// - `description` may be NULL to omit it.
1723/// - `coded_width` / `coded_height` may be NULL to omit them.
1724///
1725/// Returns a zero on success, or a negative code on failure.
1726///
1727/// # Safety
1728/// - The caller must ensure that `config` points to a valid [moq_video_config].
1729/// - The caller must ensure each non-NULL pointer inside `config` is valid for its length.
1730#[unsafe(no_mangle)]
1731pub unsafe extern "C" fn moq_publish_video_config(broadcast: u32, config: *const moq_video_config) -> i32 {
1732	ffi::enter(move || {
1733		let broadcast = ffi::parse_id(broadcast)?;
1734		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1735
1736		let name = unsafe { ffi::parse_str(config.name, config.name_len)? };
1737		let codec = unsafe { ffi::parse_str(config.codec, config.codec_len)? };
1738		let codec = hang::catalog::VideoCodec::from_str(codec).map_err(Error::Hang)?;
1739
1740		let mut video = hang::catalog::VideoConfig::new(codec);
1741		if !config.description.is_null() {
1742			let description = unsafe { ffi::parse_slice(config.description, config.description_len)? };
1743			video.description = Some(bytes::Bytes::copy_from_slice(description));
1744		}
1745		video.coded_width = unsafe { config.coded_width.as_ref() }.copied();
1746		video.coded_height = unsafe { config.coded_height.as_ref() }.copied();
1747
1748		State::lock().publish.video_config(broadcast, name, video)
1749	})
1750}
1751
1752/// Add or replace an audio rendition in a broadcast's catalog.
1753///
1754/// This is the producer counterpart to [moq_consume_audio_config]. The rendition
1755/// is keyed by `config.name`; calling this again with the same name replaces it.
1756/// The updated catalog is published to subscribers automatically.
1757///
1758/// The struct fields are read as inputs:
1759/// - `name` / `codec` are required (NOT NULL terminated) string slices.
1760/// - `sample_rate` / `channel_count` are required.
1761/// - `description` may be NULL to omit it.
1762///
1763/// Returns a zero on success, or a negative code on failure.
1764///
1765/// # Safety
1766/// - The caller must ensure that `config` points to a valid [moq_audio_config].
1767/// - The caller must ensure each non-NULL pointer inside `config` is valid for its length.
1768#[unsafe(no_mangle)]
1769pub unsafe extern "C" fn moq_publish_audio_config(broadcast: u32, config: *const moq_audio_config) -> i32 {
1770	ffi::enter(move || {
1771		let broadcast = ffi::parse_id(broadcast)?;
1772		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1773
1774		let name = unsafe { ffi::parse_str(config.name, config.name_len)? };
1775		let codec = unsafe { ffi::parse_str(config.codec, config.codec_len)? };
1776		let codec = hang::catalog::AudioCodec::from_str(codec).map_err(Error::Hang)?;
1777
1778		let mut audio = hang::catalog::AudioConfig::new(codec, config.sample_rate, config.channel_count);
1779		if !config.description.is_null() {
1780			let description = unsafe { ffi::parse_slice(config.description, config.description_len)? };
1781			audio.description = Some(bytes::Bytes::copy_from_slice(description));
1782		}
1783
1784		State::lock().publish.audio_config(broadcast, name, audio)
1785	})
1786}
1787
1788/// Remove a video rendition from a broadcast's catalog by name.
1789///
1790/// This is a no-op if no rendition with that name exists. The updated catalog is
1791/// published to subscribers automatically.
1792///
1793/// Returns a zero on success, or a negative code on failure.
1794///
1795/// # Safety
1796/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
1797#[unsafe(no_mangle)]
1798pub unsafe extern "C" fn moq_publish_video_remove(broadcast: u32, name: *const c_char, name_len: usize) -> i32 {
1799	ffi::enter(move || {
1800		let broadcast = ffi::parse_id(broadcast)?;
1801		let name = unsafe { ffi::parse_str(name, name_len)? };
1802		State::lock().publish.video_remove(broadcast, name)
1803	})
1804}
1805
1806/// Remove an audio rendition from a broadcast's catalog by name.
1807///
1808/// This is a no-op if no rendition with that name exists. The updated catalog is
1809/// published to subscribers automatically.
1810///
1811/// Returns a zero on success, or a negative code on failure.
1812///
1813/// # Safety
1814/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
1815#[unsafe(no_mangle)]
1816pub unsafe extern "C" fn moq_publish_audio_remove(broadcast: u32, name: *const c_char, name_len: usize) -> i32 {
1817	ffi::enter(move || {
1818		let broadcast = ffi::parse_id(broadcast)?;
1819		let name = unsafe { ffi::parse_str(name, name_len)? };
1820		State::lock().publish.audio_remove(broadcast, name)
1821	})
1822}
1823
1824/// Set (or replace) a top-level application catalog section by name.
1825///
1826/// This is the producer counterpart to [moq_consume_catalog_section] /
1827/// [moq_consume_catalog_section_at]: it writes an arbitrary top-level JSON key into the
1828/// catalog of a broadcast created with [moq_origin_publish], beyond the
1829/// `video`/`audio` keys owned by the media pipeline. Calling it again with the
1830/// same name replaces the section. The updated catalog is published to
1831/// subscribers automatically.
1832///
1833/// `json` is a JSON document (object, array, string, ...) as `json_len` bytes of
1834/// UTF-8. Returns a zero on success, or a negative code on failure: invalid JSON
1835/// yields a Json error (-37); a reserved `name` (`video`/`audio`) yields a mux error.
1836///
1837/// # Safety
1838/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
1839/// - The caller must ensure that json is a valid pointer to json_len bytes of data.
1840#[unsafe(no_mangle)]
1841pub unsafe extern "C" fn moq_publish_catalog_section(
1842	broadcast: u32,
1843	name: *const c_char,
1844	name_len: usize,
1845	json: *const c_char,
1846	json_len: usize,
1847) -> i32 {
1848	ffi::enter(move || {
1849		let broadcast = ffi::parse_id(broadcast)?;
1850		let name = unsafe { ffi::parse_str(name, name_len)? };
1851		let json = unsafe { ffi::parse_str(json, json_len)? };
1852		let value: serde_json::Value = serde_json::from_str(json)?;
1853		State::lock().publish.catalog_section_set(broadcast, name, value)
1854	})
1855}
1856
1857/// Remove a top-level application catalog section by name.
1858///
1859/// This is a no-op if no section with that name exists. The updated catalog is
1860/// published to subscribers automatically.
1861///
1862/// Returns a zero on success, or a negative code on failure.
1863///
1864/// # Safety
1865/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
1866#[unsafe(no_mangle)]
1867pub unsafe extern "C" fn moq_publish_catalog_section_remove(
1868	broadcast: u32,
1869	name: *const c_char,
1870	name_len: usize,
1871) -> i32 {
1872	ffi::enter(move || {
1873		let broadcast = ffi::parse_id(broadcast)?;
1874		let name = unsafe { ffi::parse_str(name, name_len)? };
1875		State::lock().publish.catalog_section_remove(broadcast, name)
1876	})
1877}
1878
1879/// Create a raw track on a broadcast for arbitrary byte payloads.
1880///
1881/// Unlike [moq_publish_media], this is the bare moq-net primitive: no
1882/// codec, container, or catalog framing. Frames written to it are delivered
1883/// as-is to subscribers using [moq_consume_track]. Use it for non-media tracks
1884/// (control channels, JSON metadata, etc.), or pair it with
1885/// [moq_publish_video_config] / [moq_publish_audio_config] to also describe the
1886/// track in the catalog. Pass NULL for `info` to use moq-net defaults.
1887///
1888/// Returns a non-zero handle to the track on success, or a negative code on failure.
1889///
1890/// # Safety
1891/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
1892/// - The caller must ensure that info is either NULL or a valid pointer to a [moq_track_info] struct.
1893#[unsafe(no_mangle)]
1894pub unsafe extern "C" fn moq_publish_track(
1895	broadcast: u32,
1896	name: *const c_char,
1897	name_len: usize,
1898	info: *const moq_track_info,
1899) -> i32 {
1900	ffi::enter(move || {
1901		let broadcast = ffi::parse_id(broadcast)?;
1902		let name = unsafe { ffi::parse_str(name, name_len)? };
1903		// Default raw tracks to a microsecond timescale even when no info is given.
1904		let info = match unsafe { info.as_ref() } {
1905			Some(info) => moq_net::track::Info::try_from(info)?,
1906			None => moq_net::track::Info::default().with_timescale(moq_net::Timescale::MICRO),
1907		};
1908		State::lock().publish.track(broadcast, name, Some(info))
1909	})
1910}
1911
1912/// Append a new group to a raw track, returning a group producer.
1913///
1914/// Groups are delivered independently and each may contain any number of frames
1915/// written via [moq_publish_group_frame]. Sequence numbers auto-increment.
1916///
1917/// Returns a non-zero handle to the group on success, or a negative code on failure.
1918#[unsafe(no_mangle)]
1919pub extern "C" fn moq_publish_track_group(track: u32) -> i32 {
1920	ffi::enter(move || {
1921		let track = ffi::parse_id(track)?;
1922		State::lock().publish.track_group(track)
1923	})
1924}
1925
1926/// Create a raw group with an explicit sequence number.
1927///
1928/// Returns a non-zero group handle on success, or a negative code on failure.
1929#[unsafe(no_mangle)]
1930pub extern "C" fn moq_publish_track_group_at(track: u32, sequence: u64) -> i32 {
1931	ffi::enter(move || {
1932		let track = ffi::parse_id(track)?;
1933		State::lock().publish.track_group_at(track, sequence)
1934	})
1935}
1936
1937/// Write a single-frame group to a raw track with a timestamp.
1938///
1939/// Convenience for the common one-frame-per-group pattern. Equivalent to
1940/// appending a group, writing one frame, and finishing it.
1941/// The timestamp is in microseconds.
1942///
1943/// Returns a zero on success, or a negative code on failure.
1944///
1945/// # Safety
1946/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
1947#[unsafe(no_mangle)]
1948pub unsafe extern "C" fn moq_publish_track_frame(
1949	track: u32,
1950	payload: *const u8,
1951	payload_size: usize,
1952	timestamp_us: u64,
1953) -> i32 {
1954	ffi::enter(move || {
1955		let track = ffi::parse_id(track)?;
1956		let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
1957		let timestamp = moq_net::Timestamp::from_micros(timestamp_us)?;
1958		State::lock().publish.track_frame(track, timestamp, payload)
1959	})
1960}
1961
1962/// Send a best-effort datagram on a raw track created by [moq_publish_track].
1963///
1964/// Takes `payload` then `timestamp_us`, matching [moq_publish_track_frame]. The payload must
1965/// be at most 1200 bytes. On success the datagram's per-track sequence number (shared with the
1966/// group namespace) is written to `out_sequence` when it is non-NULL. Datagrams are
1967/// delivered only on transports and wire versions with a datagram channel; there is no
1968/// group fallback.
1969///
1970/// Returns a zero on success, or a negative code on failure.
1971///
1972/// # Safety
1973/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
1974/// - `out_sequence` must be NULL or a valid pointer to a `uint64_t`.
1975#[unsafe(no_mangle)]
1976pub unsafe extern "C" fn moq_publish_track_datagram(
1977	track: u32,
1978	payload: *const u8,
1979	payload_size: usize,
1980	timestamp_us: u64,
1981	out_sequence: *mut u64,
1982) -> i32 {
1983	ffi::enter(move || {
1984		let track = ffi::parse_id(track)?;
1985		let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
1986		let sequence = State::lock().publish.track_datagram(track, timestamp_us, payload)?;
1987		if let Some(out) = unsafe { out_sequence.as_mut() } {
1988			*out = sequence;
1989		}
1990		Ok(())
1991	})
1992}
1993
1994/// Finish a raw track. No more groups or frames can be written.
1995///
1996/// Returns a zero on success, or a negative code on failure.
1997#[unsafe(no_mangle)]
1998pub extern "C" fn moq_publish_track_finish(track: u32) -> i32 {
1999	ffi::enter(move || {
2000		let track = ffi::parse_id(track)?;
2001		State::lock().publish.track_finish(track)
2002	})
2003}
2004
2005/// Declare a raw track's exclusive final group sequence.
2006///
2007/// Groups below `final_sequence` may still be created. Groups at or above it
2008/// are rejected. The track remains open for groups below the boundary. Call
2009/// [moq_publish_track_finish] after producing the remaining groups.
2010#[unsafe(no_mangle)]
2011pub extern "C" fn moq_publish_track_finish_at(track: u32, final_sequence: u64) -> i32 {
2012	ffi::enter(move || {
2013		let track = ffi::parse_id(track)?;
2014		State::lock().publish.track_finish_at(track, final_sequence)
2015	})
2016}
2017
2018/// Abort a raw track with an application error code.
2019#[unsafe(no_mangle)]
2020pub extern "C" fn moq_publish_track_abort(track: u32, error_code: u16) -> i32 {
2021	ffi::enter(move || {
2022		let track = ffi::parse_id(track)?;
2023		State::lock().publish.track_abort(track, error_code)
2024	})
2025}
2026
2027/// Write a frame into a raw group created by [moq_publish_track_group].
2028///
2029/// The timestamp is in microseconds.
2030///
2031/// Returns a zero on success, or a negative code on failure.
2032///
2033/// # Safety
2034/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
2035#[unsafe(no_mangle)]
2036pub unsafe extern "C" fn moq_publish_group_frame(
2037	group: u32,
2038	payload: *const u8,
2039	payload_size: usize,
2040	timestamp_us: u64,
2041) -> i32 {
2042	ffi::enter(move || {
2043		let group = ffi::parse_id(group)?;
2044		let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
2045		let timestamp = moq_net::Timestamp::from_micros(timestamp_us)?;
2046		State::lock().publish.group_frame(group, timestamp, payload)
2047	})
2048}
2049
2050/// Finish a raw group. No more frames can be written.
2051///
2052/// Returns a zero on success, or a negative code on failure.
2053#[unsafe(no_mangle)]
2054pub extern "C" fn moq_publish_group_finish(group: u32) -> i32 {
2055	ffi::enter(move || {
2056		let group = ffi::parse_id(group)?;
2057		State::lock().publish.group_finish(group)
2058	})
2059}
2060
2061/// Abort a raw group with an application error code.
2062#[unsafe(no_mangle)]
2063pub extern "C" fn moq_publish_group_abort(group: u32, error_code: u16) -> i32 {
2064	ffi::enter(move || {
2065		let group = ffi::parse_id(group)?;
2066		State::lock().publish.group_abort(group, error_code)
2067	})
2068}
2069
2070/// Create a JSON snapshot track (lossy latest-value) on a broadcast.
2071///
2072/// Values published via [moq_publish_json_snapshot_update] reach subscribers as a single latest
2073/// state; a late joiner only sees the newest. Advertise the track in the catalog with
2074/// [moq_publish_catalog_section] if consumers should discover it.
2075///
2076/// Returns a non-zero handle to the JSON producer on success, or a negative code on failure.
2077///
2078/// # Safety
2079/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
2080#[unsafe(no_mangle)]
2081pub unsafe extern "C" fn moq_publish_json_snapshot(
2082	broadcast: u32,
2083	name: *const c_char,
2084	name_len: usize,
2085	config: *const moq_json_snapshot_config,
2086) -> i32 {
2087	ffi::enter(move || {
2088		let broadcast = ffi::parse_id(broadcast)?;
2089		let name = unsafe { ffi::parse_str(name, name_len)? };
2090		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
2091		let mut producer = moq_json::snapshot::ProducerConfig::default();
2092		producer.delta_ratio = config.delta_ratio;
2093		producer.compression = config.compression;
2094		State::lock().publish.json_snapshot(broadcast, name, producer)
2095	})
2096}
2097
2098/// Publish a new value to a JSON snapshot track. `value` is a UTF-8 JSON document. A no-op if
2099/// unchanged from the previous update.
2100///
2101/// Returns a zero on success, or a negative code on failure.
2102///
2103/// # Safety
2104/// - The caller must ensure `value` is a valid pointer to `value_len` bytes.
2105#[unsafe(no_mangle)]
2106pub unsafe extern "C" fn moq_publish_json_snapshot_update(json: u32, value: *const c_char, value_len: usize) -> i32 {
2107	ffi::enter(move || {
2108		let json = ffi::parse_id(json)?;
2109		let value = unsafe { ffi::parse_slice(value.cast::<u8>(), value_len)? };
2110		let value = serde_json::from_slice(value)?;
2111		State::lock().publish.json_snapshot_update(json, value)
2112	})
2113}
2114
2115/// Finish a JSON snapshot track. No more values can be published.
2116///
2117/// Returns a zero on success, or a negative code on failure.
2118#[unsafe(no_mangle)]
2119pub extern "C" fn moq_publish_json_snapshot_finish(json: u32) -> i32 {
2120	ffi::enter(move || {
2121		let json = ffi::parse_id(json)?;
2122		State::lock().publish.json_snapshot_finish(json)
2123	})
2124}
2125
2126/// Create a JSON stream track (lossless append-log) on a broadcast.
2127///
2128/// Every record appended via [moq_publish_json_stream_append] is preserved and delivered in order.
2129///
2130/// Returns a non-zero handle to the JSON stream producer on success, or a negative code on failure.
2131///
2132/// # Safety
2133/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
2134#[unsafe(no_mangle)]
2135pub unsafe extern "C" fn moq_publish_json_stream(
2136	broadcast: u32,
2137	name: *const c_char,
2138	name_len: usize,
2139	config: *const moq_json_stream_config,
2140) -> i32 {
2141	ffi::enter(move || {
2142		let broadcast = ffi::parse_id(broadcast)?;
2143		let name = unsafe { ffi::parse_str(name, name_len)? };
2144		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
2145		let producer = moq_json::stream::ProducerConfig::default().with_compression(config.compression);
2146		State::lock().publish.json_stream(broadcast, name, producer)
2147	})
2148}
2149
2150/// Append one record to a JSON stream track. `value` is a UTF-8 JSON document.
2151///
2152/// Returns a zero on success, or a negative code on failure.
2153///
2154/// # Safety
2155/// - The caller must ensure `value` is a valid pointer to `value_len` bytes.
2156#[unsafe(no_mangle)]
2157pub unsafe extern "C" fn moq_publish_json_stream_append(stream: u32, value: *const c_char, value_len: usize) -> i32 {
2158	ffi::enter(move || {
2159		let stream = ffi::parse_id(stream)?;
2160		let value = unsafe { ffi::parse_slice(value.cast::<u8>(), value_len)? };
2161		let value = serde_json::from_slice(value)?;
2162		State::lock().publish.json_stream_append(stream, value)
2163	})
2164}
2165
2166/// Finish a JSON stream track. No more records can be appended.
2167///
2168/// Returns a zero on success, or a negative code on failure.
2169#[unsafe(no_mangle)]
2170pub extern "C" fn moq_publish_json_stream_finish(stream: u32) -> i32 {
2171	ffi::enter(move || {
2172		let stream = ffi::parse_id(stream)?;
2173		State::lock().publish.json_stream_finish(stream)
2174	})
2175}
2176
2177/// Create a catalog consumer for a broadcast.
2178///
2179/// `on_catalog` is invoked with a positive catalog ID for each catalog update
2180/// (usable to query video/audio track information), then exactly once more with
2181/// a terminal code: `0` (closed cleanly) or a negative error. After the terminal
2182/// (`<= 0`) callback, `on_catalog` is never called again and `user_data` is never
2183/// touched again, so release `user_data` there. The terminal callback fires even
2184/// after [moq_consume_catalog_close].
2185///
2186/// Returns a non-zero handle on success, or a negative code on failure.
2187///
2188/// # Safety
2189/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_catalog` callback.
2190#[unsafe(no_mangle)]
2191pub unsafe extern "C" fn moq_consume_catalog(
2192	broadcast: u32,
2193	on_catalog: Option<extern "C" fn(user_data: *mut c_void, catalog: i32)>,
2194	user_data: *mut c_void,
2195) -> i32 {
2196	ffi::enter(move || {
2197		let broadcast = ffi::parse_id(broadcast)?;
2198		let on_catalog = unsafe { ffi::OnStatus::new(user_data, on_catalog) };
2199		State::lock().consume.catalog(broadcast, on_catalog)
2200	})
2201}
2202
2203/// Stop a catalog consumer's background subscription.
2204///
2205/// Returns immediately: zero on success, or a negative code if already closed.
2206/// Does NOT free `user_data`; the [moq_consume_catalog] callback still fires once
2207/// more with a terminal `0` (or a negative error), which is where `user_data`
2208/// should be released. Catalog snapshots previously delivered via the callback
2209/// remain valid until freed with [moq_consume_catalog_free].
2210#[unsafe(no_mangle)]
2211pub extern "C" fn moq_consume_catalog_close(catalog: u32) -> i32 {
2212	ffi::enter(move || {
2213		let catalog = ffi::parse_id(catalog)?;
2214		State::lock().consume.catalog_close(catalog)
2215	})
2216}
2217
2218/// Free a catalog snapshot received via the [moq_consume_catalog] callback.
2219///
2220/// This releases the snapshot and invalidates any borrowed references (e.g. pointers
2221/// returned by [moq_consume_video_config] or [moq_consume_audio_config]).
2222///
2223/// Returns a zero on success, or a negative code on failure.
2224#[unsafe(no_mangle)]
2225pub extern "C" fn moq_consume_catalog_free(catalog: u32) -> i32 {
2226	ffi::enter(move || {
2227		let catalog = ffi::parse_id(catalog)?;
2228		State::lock().consume.catalog_free(catalog)
2229	})
2230}
2231
2232/// Query information about a video track in a catalog.
2233///
2234/// The destination is filled with the video track information.
2235///
2236/// Returns a zero on success, or a negative code on failure.
2237///
2238/// # Safety
2239/// - The caller must ensure that `dst` is a valid pointer to a [moq_video_config] struct.
2240/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
2241#[unsafe(no_mangle)]
2242pub unsafe extern "C" fn moq_consume_video_config(catalog: u32, index: u32, dst: *mut moq_video_config) -> i32 {
2243	ffi::enter(move || {
2244		let catalog = ffi::parse_id(catalog)?;
2245		let index = index as usize;
2246		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2247		State::lock().consume.video_config(catalog, index, dst)
2248	})
2249}
2250
2251/// Query the catalog properties shared by every video rendition.
2252///
2253/// The destination is filled by value and remains valid after the catalog snapshot is freed.
2254/// Inspect each `has_*` flag before reading its value.
2255///
2256/// Returns zero on success, or a negative code on failure.
2257///
2258/// # Safety
2259/// - The caller must ensure that `dst` points to a valid [moq_video_properties].
2260#[unsafe(no_mangle)]
2261pub unsafe extern "C" fn moq_consume_video_properties(catalog: u32, dst: *mut moq_video_properties) -> i32 {
2262	ffi::enter(move || {
2263		let catalog = ffi::parse_id(catalog)?;
2264		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2265		State::lock().consume.video_properties(catalog, dst)
2266	})
2267}
2268
2269/// Query information about an audio track in a catalog.
2270///
2271/// The destination is filled with the audio track information.
2272///
2273/// Returns a zero on success, or a negative code on failure.
2274///
2275/// # Safety
2276/// - The caller must ensure that `dst` is a valid pointer to a [moq_audio_config] struct.
2277/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
2278#[unsafe(no_mangle)]
2279pub unsafe extern "C" fn moq_consume_audio_config(catalog: u32, index: u32, dst: *mut moq_audio_config) -> i32 {
2280	ffi::enter(move || {
2281		let catalog = ffi::parse_id(catalog)?;
2282		let index = index as usize;
2283		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2284		State::lock().consume.audio_config(catalog, index, dst)
2285	})
2286}
2287
2288/// Number of untyped application catalog sections in a catalog snapshot.
2289///
2290/// These are the top-level catalog keys beyond `video`/`audio`, carried through
2291/// verbatim. Iterate them by index with [moq_consume_catalog_section_at], or look one up
2292/// directly by name with [moq_consume_catalog_section].
2293///
2294/// Returns the count (>= 0) on success, or a negative code on failure.
2295#[unsafe(no_mangle)]
2296pub extern "C" fn moq_consume_catalog_section_count(catalog: u32) -> i32 {
2297	ffi::enter(move || {
2298		let catalog = ffi::parse_id(catalog)?;
2299		State::lock().consume.catalog_section_count(catalog)
2300	})
2301}
2302
2303/// Query an application catalog section by index, keyed by name.
2304///
2305/// Fills `dst` with the section's name and JSON value at `index`, in the range
2306/// `[0, moq_consume_catalog_section_count)`. Both pointers borrow the snapshot's storage
2307/// and stay valid until it is freed with [moq_consume_catalog_free].
2308///
2309/// Returns a zero on success, or a negative code on failure (e.g. `index` out of
2310/// range).
2311///
2312/// # Safety
2313/// - The caller must ensure that `dst` is a valid pointer to a [moq_section] struct.
2314/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
2315#[unsafe(no_mangle)]
2316pub unsafe extern "C" fn moq_consume_catalog_section_at(catalog: u32, index: u32, dst: *mut moq_section) -> i32 {
2317	ffi::enter(move || {
2318		let catalog = ffi::parse_id(catalog)?;
2319		let index = index as usize;
2320		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2321		State::lock().consume.catalog_section_at(catalog, index, dst)
2322	})
2323}
2324
2325/// Look up an application catalog section by name.
2326///
2327/// Fills `dst` with the section's JSON value (the document to parse yourself).
2328/// The pointer borrows the snapshot's storage and stays valid until it is freed
2329/// with [moq_consume_catalog_free].
2330///
2331/// Returns a zero on success, or a negative code on failure: no section with that
2332/// name yields a not-found error.
2333///
2334/// # Safety
2335/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
2336/// - The caller must ensure that `dst` is a valid pointer to a [moq_string] struct.
2337/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
2338#[unsafe(no_mangle)]
2339pub unsafe extern "C" fn moq_consume_catalog_section(
2340	catalog: u32,
2341	name: *const c_char,
2342	name_len: usize,
2343	dst: *mut moq_string,
2344) -> i32 {
2345	ffi::enter(move || {
2346		let catalog = ffi::parse_id(catalog)?;
2347		let name = unsafe { ffi::parse_str(name, name_len)? };
2348		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2349		State::lock().consume.catalog_section_get(catalog, name, dst)
2350	})
2351}
2352
2353/// Consume a video track from a broadcast, delivering frames in order.
2354///
2355/// - `max_latency_ms` controls the maximum amount of buffering allowed before skipping a GoP.
2356/// - `on_frame` is called with a positive frame ID per frame, then exactly once
2357///   more with a terminal code: `0` (closed cleanly) or a negative error. After
2358///   the terminal (`<= 0`) callback, `on_frame` is never called again and
2359///   `user_data` is never touched again, so release `user_data` there. The
2360///   terminal callback fires even after [moq_consume_video_close].
2361///
2362/// Returns a non-zero handle to the track on success, or a negative code on failure.
2363///
2364/// # Safety
2365/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
2366#[unsafe(no_mangle)]
2367pub unsafe extern "C" fn moq_consume_video(
2368	catalog: u32,
2369	index: u32,
2370	max_latency_ms: u64,
2371	on_frame: Option<extern "C" fn(user_data: *mut c_void, frame: i32)>,
2372	user_data: *mut c_void,
2373) -> i32 {
2374	ffi::enter(move || {
2375		let catalog = ffi::parse_id(catalog)?;
2376		let index = index as usize;
2377		let max_latency = std::time::Duration::from_millis(max_latency_ms);
2378		let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame) };
2379		State::lock().consume.video(catalog, index, max_latency, on_frame)
2380	})
2381}
2382
2383/// Stop a video track consumer's background task.
2384///
2385/// Returns immediately: zero on success, or a negative code if already closed.
2386/// Does NOT free `user_data`; the [moq_consume_video] `on_frame` callback
2387/// still fires once more with a terminal `0` (or a negative error), which is
2388/// where `user_data` should be released.
2389#[unsafe(no_mangle)]
2390pub extern "C" fn moq_consume_video_close(track: u32) -> i32 {
2391	ffi::enter(move || {
2392		let track = ffi::parse_id(track)?;
2393		State::lock().consume.track_close(track)
2394	})
2395}
2396
2397/// Consume an audio track from a broadcast, emitting the frames in order.
2398///
2399/// `on_frame` is called with a positive frame ID per frame, then exactly once
2400/// more with a terminal code: `0` (closed cleanly) or a negative error. After
2401/// the terminal (`<= 0`) callback, `on_frame` is never called again and
2402/// `user_data` is never touched again, so release `user_data` there. The
2403/// terminal callback fires even after [moq_consume_audio_close].
2404/// The `max_latency_ms` parameter controls how long to wait before skipping frames.
2405///
2406/// Returns a non-zero handle to the track on success, or a negative code on failure.
2407///
2408/// # Safety
2409/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
2410#[unsafe(no_mangle)]
2411pub unsafe extern "C" fn moq_consume_audio(
2412	catalog: u32,
2413	index: u32,
2414	max_latency_ms: u64,
2415	on_frame: Option<extern "C" fn(user_data: *mut c_void, frame: i32)>,
2416	user_data: *mut c_void,
2417) -> i32 {
2418	ffi::enter(move || {
2419		let catalog = ffi::parse_id(catalog)?;
2420		let index = index as usize;
2421		let max_latency = std::time::Duration::from_millis(max_latency_ms);
2422		let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame) };
2423		State::lock().consume.audio(catalog, index, max_latency, on_frame)
2424	})
2425}
2426
2427/// Stop an audio track consumer's background task.
2428///
2429/// Returns immediately: zero on success, or a negative code if already closed.
2430/// Does NOT free `user_data`; the [moq_consume_audio] `on_frame` callback
2431/// still fires once more with a terminal `0` (or a negative error), which is
2432/// where `user_data` should be released.
2433#[unsafe(no_mangle)]
2434pub extern "C" fn moq_consume_audio_close(track: u32) -> i32 {
2435	ffi::enter(move || {
2436		let track = ffi::parse_id(track)?;
2437		State::lock().consume.track_close(track)
2438	})
2439}
2440
2441/// Get a chunk of a frame's payload.
2442///
2443/// Read the payload of a frame as a single contiguous slice.
2444///
2445/// Frames are not chunked; the entire payload is delivered through `dst.payload` /
2446/// `dst.payload_size` in one call. The pointer is valid until [`moq_consume_frame_free`]
2447/// is called for this frame.
2448///
2449/// Returns a zero on success, or a negative code on failure.
2450///
2451/// # Safety
2452/// - The caller must ensure that `dst` is a valid pointer to a [moq_frame] struct.
2453#[unsafe(no_mangle)]
2454pub unsafe extern "C" fn moq_consume_frame(frame: u32, dst: *mut moq_frame) -> i32 {
2455	ffi::enter(move || {
2456		let frame = ffi::parse_id(frame)?;
2457		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2458		State::lock().consume.frame(frame, dst)
2459	})
2460}
2461
2462/// Free a decoded frame delivered via a [moq_consume_video] or [moq_consume_audio] callback.
2463///
2464/// Returns a zero on success, or a negative code on failure.
2465#[unsafe(no_mangle)]
2466pub extern "C" fn moq_consume_frame_free(frame: u32) -> i32 {
2467	ffi::enter(move || {
2468		let frame = ffi::parse_id(frame)?;
2469		State::lock().consume.frame_close(frame)
2470	})
2471}
2472
2473/// Close a broadcast consumer and clean up its resources.
2474///
2475/// Returns a zero on success, or a negative code on failure.
2476#[unsafe(no_mangle)]
2477pub extern "C" fn moq_consume_close(consume: u32) -> i32 {
2478	ffi::enter(move || {
2479		let consume = ffi::parse_id(consume)?;
2480		State::lock().consume.close(consume)
2481	})
2482}
2483
2484/// Subscribe to a raw track by name, delivering each frame's payload as-is.
2485///
2486/// This is the counterpart to [moq_publish_track]: no catalog lookup or
2487/// container parsing. `on_frame` is called with a positive raw frame ID for each
2488/// frame in sequence order, then exactly once more with a terminal code: `0`
2489/// (closed cleanly) or a negative error. After the terminal (`<= 0`) callback,
2490/// `on_frame` is never called again and `user_data` is never touched again, so
2491/// release `user_data` there. The terminal callback fires even after
2492/// [moq_consume_track_close]. Read each frame with [moq_consume_track_frame] and
2493/// release it with [moq_consume_track_frame_free]. Pass NULL for `subscription`
2494/// to use moq-net defaults.
2495///
2496/// Returns a non-zero handle to the track on success, or a negative code on failure.
2497///
2498/// # Safety
2499/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
2500/// - The caller must ensure that subscription is either NULL or a valid pointer to a [moq_subscription] struct.
2501/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
2502#[unsafe(no_mangle)]
2503pub unsafe extern "C" fn moq_consume_track(
2504	broadcast: u32,
2505	name: *const c_char,
2506	name_len: usize,
2507	subscription: *const moq_subscription,
2508	on_frame: Option<extern "C" fn(user_data: *mut c_void, frame: i32)>,
2509	user_data: *mut c_void,
2510) -> i32 {
2511	ffi::enter(move || {
2512		let broadcast = ffi::parse_id(broadcast)?;
2513		let name = unsafe { ffi::parse_str(name, name_len)? };
2514		let subscription = unsafe { subscription.as_ref() }.map(moq_net::track::Subscription::from);
2515		let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame) };
2516		State::lock().consume.raw_track(broadcast, name, subscription, on_frame)
2517	})
2518}
2519
2520/// Update a raw track subscription's delivery preferences.
2521///
2522/// Pass NULL for `subscription` to reset to moq-net defaults.
2523///
2524/// Returns a zero on success, or a negative code on failure.
2525///
2526/// # Safety
2527/// - The caller must ensure that subscription is either NULL or a valid pointer to a [moq_subscription] struct.
2528#[unsafe(no_mangle)]
2529pub unsafe extern "C" fn moq_consume_track_update(track: u32, subscription: *const moq_subscription) -> i32 {
2530	ffi::enter(move || {
2531		let track = ffi::parse_id(track)?;
2532		let subscription = unsafe { subscription.as_ref() }.map(moq_net::track::Subscription::from);
2533		State::lock().consume.raw_track_update(track, subscription)
2534	})
2535}
2536
2537/// Read a raw frame's payload delivered via the [moq_consume_track] callback.
2538///
2539/// Fills `dst.payload` / `dst.payload_size`; the pointer is valid until the
2540/// frame is released with [moq_consume_frame_free]. `dst.timestamp_us` is the
2541/// frame presentation timestamp in microseconds. `dst.keyframe` is reported as
2542/// false because raw tracks do not parse codec metadata.
2543///
2544/// Returns a zero on success, or a negative code on failure.
2545///
2546/// # Safety
2547/// - The caller must ensure that `dst` is a valid pointer to a [moq_frame] struct.
2548#[unsafe(no_mangle)]
2549pub unsafe extern "C" fn moq_consume_track_frame(frame: u32, dst: *mut moq_frame) -> i32 {
2550	ffi::enter(move || {
2551		let frame = ffi::parse_id(frame)?;
2552		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2553		State::lock().consume.raw_frame(frame, dst)
2554	})
2555}
2556
2557/// Free a raw frame delivered via the [moq_consume_track] callback, releasing its payload.
2558///
2559/// Returns a zero on success, or a negative code on failure.
2560#[unsafe(no_mangle)]
2561pub extern "C" fn moq_consume_track_frame_free(frame: u32) -> i32 {
2562	ffi::enter(move || {
2563		let frame = ffi::parse_id(frame)?;
2564		State::lock().consume.raw_frame_close(frame)
2565	})
2566}
2567
2568/// Stop a raw track consumer's background task.
2569///
2570/// Returns immediately: zero on success, or a negative code if already closed.
2571/// Does NOT free `user_data`; the [moq_consume_track] `on_frame` callback still
2572/// fires once more with a terminal `0` (or a negative error), which is where
2573/// `user_data` should be released. Frames already delivered via the callback
2574/// remain valid until released with [moq_consume_track_frame_free].
2575#[unsafe(no_mangle)]
2576pub extern "C" fn moq_consume_track_close(track: u32) -> i32 {
2577	ffi::enter(move || {
2578		let track = ffi::parse_id(track)?;
2579		State::lock().consume.raw_track_close(track)
2580	})
2581}
2582
2583/// Subscribe to a raw track's best-effort datagrams by name.
2584///
2585/// The datagram counterpart to [moq_consume_track], on its own subscription. `on_datagram`
2586/// is called with a positive datagram ID for each datagram in arrival order, then exactly
2587/// once more with a terminal code: `0` (closed cleanly) or a negative error. After the
2588/// terminal (`<= 0`) callback, `on_datagram` is never called again and `user_data` is never
2589/// touched again, so release `user_data` there. The terminal callback fires even after
2590/// [moq_consume_datagrams_close]. Read each datagram with [moq_consume_datagram] and release
2591/// it with [moq_consume_datagram_free]. Datagrams arrive only over datagram-capable
2592/// transports and lite-05 or newer moq-lite; there is no stream fallback.
2593///
2594/// Returns a non-zero handle to the subscription on success, or a negative code on failure.
2595///
2596/// # Safety
2597/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
2598/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_datagram` callback.
2599#[unsafe(no_mangle)]
2600pub unsafe extern "C" fn moq_consume_datagrams(
2601	broadcast: u32,
2602	name: *const c_char,
2603	name_len: usize,
2604	on_datagram: Option<extern "C" fn(user_data: *mut c_void, datagram: i32)>,
2605	user_data: *mut c_void,
2606) -> i32 {
2607	ffi::enter(move || {
2608		let broadcast = ffi::parse_id(broadcast)?;
2609		let name = unsafe { ffi::parse_str(name, name_len)? };
2610		let on_datagram = unsafe { ffi::OnStatus::new(user_data, on_datagram) };
2611		State::lock().consume.datagram_track(broadcast, name, on_datagram)
2612	})
2613}
2614
2615/// Read a datagram delivered via the [moq_consume_datagrams] callback.
2616///
2617/// Fills `dst.payload` / `dst.payload_size` (valid until the datagram is released with
2618/// [moq_consume_datagram_free]), plus `dst.timestamp_us` and `dst.sequence`.
2619///
2620/// Returns a zero on success, or a negative code on failure.
2621///
2622/// # Safety
2623/// - The caller must ensure that `dst` is a valid pointer to a [moq_datagram] struct.
2624#[unsafe(no_mangle)]
2625pub unsafe extern "C" fn moq_consume_datagram(datagram: u32, dst: *mut moq_datagram) -> i32 {
2626	ffi::enter(move || {
2627		let datagram = ffi::parse_id(datagram)?;
2628		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2629		State::lock().consume.datagram(datagram, dst)
2630	})
2631}
2632
2633/// Free a datagram delivered via the [moq_consume_datagrams] callback, releasing its payload.
2634///
2635/// Returns a zero on success, or a negative code on failure.
2636#[unsafe(no_mangle)]
2637pub extern "C" fn moq_consume_datagram_free(datagram: u32) -> i32 {
2638	ffi::enter(move || {
2639		let datagram = ffi::parse_id(datagram)?;
2640		State::lock().consume.datagram_close(datagram)
2641	})
2642}
2643
2644/// Stop a datagram subscription's background task.
2645///
2646/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
2647/// `user_data`; the [moq_consume_datagrams] `on_datagram` callback still fires once more with a
2648/// terminal `0` (or a negative error), which is where `user_data` should be released. Datagrams
2649/// already delivered via the callback remain valid until released with [moq_consume_datagram_free].
2650#[unsafe(no_mangle)]
2651pub extern "C" fn moq_consume_datagrams_close(task: u32) -> i32 {
2652	ffi::enter(move || {
2653		let task = ffi::parse_id(task)?;
2654		State::lock().consume.datagram_track_close(task)
2655	})
2656}
2657
2658/// Subscribe to a JSON snapshot track (lossy latest-value) by name.
2659///
2660/// `on_value` is called with a positive value ID for each new latest value; a consumer that
2661/// falls behind collapses the backlog and only sees the newest. It is called exactly once more
2662/// with a terminal `0` (track ended / closed) or a negative error, after which `user_data` is
2663/// never touched again, so release it there. Read each value with [moq_consume_json_value] and
2664/// release it with [moq_consume_json_value_free]. Pass the same compression the producer used.
2665///
2666/// Returns a non-zero handle to the task on success, or a negative code on failure.
2667///
2668/// # Safety
2669/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
2670/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_value` callback.
2671#[unsafe(no_mangle)]
2672pub unsafe extern "C" fn moq_consume_json_snapshot(
2673	broadcast: u32,
2674	name: *const c_char,
2675	name_len: usize,
2676	config: *const moq_json_snapshot_config,
2677	on_value: Option<extern "C" fn(user_data: *mut c_void, value: i32)>,
2678	user_data: *mut c_void,
2679) -> i32 {
2680	ffi::enter(move || {
2681		let broadcast = ffi::parse_id(broadcast)?;
2682		let name = unsafe { ffi::parse_str(name, name_len)? };
2683		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
2684		let mut consumer = moq_json::snapshot::ConsumerConfig::default();
2685		consumer.compression = config.compression;
2686		let on_value = unsafe { ffi::OnStatus::new(user_data, on_value) };
2687		State::lock().consume.json_snapshot(broadcast, name, consumer, on_value)
2688	})
2689}
2690
2691/// Subscribe to a JSON stream track (lossless append-log) by name.
2692///
2693/// `on_value` is called with a positive value ID for each record, in order, then once more with
2694/// a terminal `0` or negative error where `user_data` should be released. Read each value with
2695/// [moq_consume_json_value] and release it with [moq_consume_json_value_free].
2696///
2697/// Returns a non-zero handle to the task on success, or a negative code on failure.
2698///
2699/// # Safety
2700/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
2701/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_value` callback.
2702#[unsafe(no_mangle)]
2703pub unsafe extern "C" fn moq_consume_json_stream(
2704	broadcast: u32,
2705	name: *const c_char,
2706	name_len: usize,
2707	config: *const moq_json_stream_config,
2708	on_value: Option<extern "C" fn(user_data: *mut c_void, value: i32)>,
2709	user_data: *mut c_void,
2710) -> i32 {
2711	ffi::enter(move || {
2712		let broadcast = ffi::parse_id(broadcast)?;
2713		let name = unsafe { ffi::parse_str(name, name_len)? };
2714		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
2715		let consumer = moq_json::stream::ConsumerConfig::default().with_compression(config.compression);
2716		let on_value = unsafe { ffi::OnStatus::new(user_data, on_value) };
2717		State::lock().consume.json_stream(broadcast, name, consumer, on_value)
2718	})
2719}
2720
2721/// Read a JSON value delivered via a [moq_consume_json_snapshot] or [moq_consume_json_stream] callback.
2722///
2723/// Fills `dst.json` / `dst.json_len`; the pointer is valid until the value is released with
2724/// [moq_consume_json_value_free].
2725///
2726/// Returns a zero on success, or a negative code on failure.
2727///
2728/// # Safety
2729/// - The caller must ensure `dst` is a valid pointer to a [moq_json_value] struct.
2730#[unsafe(no_mangle)]
2731pub unsafe extern "C" fn moq_consume_json_value(value: u32, dst: *mut moq_json_value) -> i32 {
2732	ffi::enter(move || {
2733		let value = ffi::parse_id(value)?;
2734		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2735		State::lock().consume.json_value(value, dst)
2736	})
2737}
2738
2739/// Release a JSON value delivered via a consumer callback.
2740///
2741/// Returns a zero on success, or a negative code on failure.
2742#[unsafe(no_mangle)]
2743pub extern "C" fn moq_consume_json_value_free(value: u32) -> i32 {
2744	ffi::enter(move || {
2745		let value = ffi::parse_id(value)?;
2746		State::lock().consume.json_value_close(value)
2747	})
2748}
2749
2750/// Stop a JSON consumer's background task (snapshot or stream).
2751///
2752/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
2753/// `user_data`; the `on_value` callback still fires once more with a terminal `0` (or a negative
2754/// error), which is where `user_data` should be released. Values already delivered remain valid
2755/// until released with [moq_consume_json_value_free].
2756#[unsafe(no_mangle)]
2757pub extern "C" fn moq_consume_json_close(task: u32) -> i32 {
2758	ffi::enter(move || {
2759		let task = ffi::parse_id(task)?;
2760		State::lock().consume.json_close(task)
2761	})
2762}