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