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