Skip to main content

moq/
api.rs

1use crate::ffi::ReturnCode;
2use crate::{Connect, Error, State, ffi, moq_protocol_error};
3
4use std::ffi::c_char;
5use std::ffi::c_void;
6use std::str::FromStr;
7
8use tracing::Level;
9
10/// How a media track's frames are wrapped, independent of the codec.
11///
12/// The ABI carries this as a `uint32_t`, so an unknown discriminant from C is an
13/// error rather than UB.
14#[repr(C)]
15#[allow(non_camel_case_types)]
16#[derive(Clone, Copy, Debug)]
17pub enum moq_container_kind {
18	/// A QUIC VarInt timestamp prefix followed by the raw codec payload.
19	/// Timestamps are in microseconds.
20	MOQ_CONTAINER_KIND_LEGACY = 0,
21	/// Fragmented MP4: each frame is a complete moof+mdat fragment, described by
22	/// the init segment in `moq_container::init`.
23	MOQ_CONTAINER_KIND_CMAF = 1,
24	/// Low Overhead Container (draft-ietf-moq-loc): a small property block
25	/// followed by the codec payload.
26	MOQ_CONTAINER_KIND_LOC = 2,
27	/// A container this build does not recognize, so the rendition must be
28	/// ignored. Only ever read out of a catalog: publishing it is an error.
29	MOQ_CONTAINER_KIND_UNKNOWN = 3,
30}
31
32/// The container of a video or audio rendition, plus whatever that container
33/// needs to describe itself.
34///
35/// Zeroing this struct means `MOQ_CONTAINER_KIND_LEGACY` with no init segment,
36/// which is what a rendition written by [moq_publish_audio] or [moq_publish_video] carries.
37#[repr(C)]
38#[allow(non_camel_case_types)]
39#[derive(Clone, Copy)]
40pub struct moq_container {
41	/// `moq_container_kind` discriminant.
42	pub kind: u32,
43
44	/// The CMAF init segment (ftyp+moov), or NULL.
45	/// Read only when `kind` is `MOQ_CONTAINER_KIND_CMAF`, where it is required.
46	pub init: *const u8,
47	pub init_len: usize,
48}
49
50impl Default for moq_container {
51	fn default() -> Self {
52		Self {
53			kind: moq_container_kind::MOQ_CONTAINER_KIND_LEGACY as u32,
54			init: std::ptr::null(),
55			init_len: 0,
56		}
57	}
58}
59
60/// # Safety
61/// - `container->init` must point to `container->init_len` bytes when
62///   `container->kind` is `MOQ_CONTAINER_KIND_CMAF`.
63pub(crate) unsafe fn parse_container(container: &moq_container) -> Result<hang::catalog::Container, Error> {
64	use hang::catalog::Container;
65
66	Ok(match container.kind {
67		v if v == moq_container_kind::MOQ_CONTAINER_KIND_LEGACY as u32 => Container::Legacy,
68		v if v == moq_container_kind::MOQ_CONTAINER_KIND_CMAF as u32 => {
69			let init = unsafe { ffi::parse_slice(container.init, container.init_len)? };
70			// A CMAF rendition is undecodable without its init segment, so an empty one
71			// fails here rather than at every subscriber.
72			if init.is_empty() {
73				return Err(Error::InvalidPointer);
74			}
75
76			Container::Cmaf {
77				init: bytes::Bytes::copy_from_slice(init),
78			}
79		}
80		v if v == moq_container_kind::MOQ_CONTAINER_KIND_LOC as u32 => Container::Loc,
81		// UNKNOWN included: we kept none of the original JSON, so there is nothing to republish.
82		_ => return Err(Error::InvalidCode),
83	})
84}
85
86/// Describe a catalog container for C, borrowing the CMAF init segment rather
87/// than copying it, so the result lives only as long as the catalog snapshot.
88pub(crate) fn borrow_container(container: &hang::catalog::Container) -> moq_container {
89	use hang::catalog::Container;
90
91	let (kind, init) = match container {
92		Container::Legacy => (moq_container_kind::MOQ_CONTAINER_KIND_LEGACY, None),
93		Container::Cmaf { init } => (moq_container_kind::MOQ_CONTAINER_KIND_CMAF, Some(init)),
94		Container::Loc => (moq_container_kind::MOQ_CONTAINER_KIND_LOC, None),
95		Container::Unknown(_) => (moq_container_kind::MOQ_CONTAINER_KIND_UNKNOWN, None),
96	};
97
98	moq_container {
99		kind: kind as u32,
100		init: init.map_or(std::ptr::null(), |init| init.as_ptr()),
101		init_len: init.map_or(0, |init| init.len()),
102	}
103}
104
105/// A single audio codec [moq_publish_audio] can parse.
106#[repr(C)]
107#[allow(non_camel_case_types)]
108#[derive(Clone, Copy, Debug, PartialEq, Eq)]
109pub enum moq_audio_format {
110	/// Advanced Audio Coding, configured by an AudioSpecificConfig.
111	MOQ_AUDIO_FORMAT_AAC = 0,
112	/// Opus, configured by an OpusHead.
113	MOQ_AUDIO_FORMAT_OPUS = 1,
114	/// FLAC, configured by the `fLaC` marker plus its STREAMINFO block.
115	MOQ_AUDIO_FORMAT_FLAC = 2,
116	/// MPEG-1/2 Audio Layer III.
117	MOQ_AUDIO_FORMAT_MP3 = 3,
118}
119
120/// A single video codec [moq_publish_video] can parse.
121///
122/// H.264 and H.265 appear twice each because the framing differs, not just the
123/// codec: AVC1/HVC1 are length-prefixed with an out-of-band config record,
124/// while AVC3/HEV1 are Annex-B with the parameter sets inline.
125#[repr(C)]
126#[allow(non_camel_case_types)]
127#[derive(Clone, Copy, Debug, PartialEq, Eq)]
128pub enum moq_video_format {
129	/// H.264, length-prefixed NALUs with an out-of-band avcC.
130	MOQ_VIDEO_FORMAT_AVC1 = 0,
131	/// H.264, Annex-B with inline SPS/PPS.
132	MOQ_VIDEO_FORMAT_AVC3 = 1,
133	/// H.265, length-prefixed NALUs with an out-of-band hvcC.
134	MOQ_VIDEO_FORMAT_HVC1 = 2,
135	/// H.265, Annex-B with inline parameter sets.
136	MOQ_VIDEO_FORMAT_HEV1 = 3,
137	/// AV1.
138	MOQ_VIDEO_FORMAT_AV01 = 4,
139	/// VP8.
140	MOQ_VIDEO_FORMAT_VP8 = 5,
141	/// VP9.
142	MOQ_VIDEO_FORMAT_VP9 = 6,
143}
144
145/// A container [moq_publish_container] can demux, which may publish several tracks.
146#[repr(C)]
147#[allow(non_camel_case_types)]
148#[derive(Clone, Copy, Debug, PartialEq, Eq)]
149pub enum moq_container_format {
150	/// Fragmented MP4 / CMAF.
151	MOQ_CONTAINER_FORMAT_FMP4 = 0,
152	/// Matroska / WebM.
153	MOQ_CONTAINER_FORMAT_MKV = 1,
154	/// MPEG-2 transport stream.
155	MOQ_CONTAINER_FORMAT_TS = 2,
156	/// Flash Video, as used by RTMP.
157	MOQ_CONTAINER_FORMAT_FLV = 3,
158}
159
160/// Configuration for [moq_publish_audio].
161///
162/// Zero the struct, then set `format` and the required `init` bytes. New
163/// optional fields are appended so existing initializers keep their meaning.
164#[repr(C)]
165#[allow(non_camel_case_types)]
166pub struct moq_audio_init {
167	/// The audio codec, a [moq_audio_format] value.
168	pub format: u32,
169
170	/// Codec init bytes: an OpusHead, an AudioSpecificConfig, a STREAMINFO.
171	/// Required, since audio has no in-band config to resolve from frames.
172	pub init: *const u8,
173	/// Length of `init` in bytes.
174	pub init_len: usize,
175
176	/// Human-readable rendition name for track pickers, or NULL if not used.
177	pub label: *const c_char,
178	/// Length of `label` in bytes.
179	pub label_len: usize,
180}
181
182/// Configuration for [moq_publish_video].
183///
184/// Zero the struct, then set `format` and whatever else the codec needs. `init`
185/// may stay NULL for a format that resolves in band.
186#[repr(C)]
187#[allow(non_camel_case_types)]
188pub struct moq_video_init {
189	/// The video codec, a [moq_video_format] value.
190	pub format: u32,
191
192	/// Codec init bytes (an avcC, an hvcC), or NULL for a format that resolves
193	/// from the stream itself.
194	pub init: *const u8,
195	/// Length of `init` in bytes.
196	pub init_len: usize,
197
198	/// Human-readable rendition name for track pickers, or NULL if not used.
199	pub label: *const c_char,
200	/// Length of `label` in bytes.
201	pub label_len: usize,
202
203	/// Catalog fields the bitstream cannot reveal itself. Zeroed means none.
204	pub hint: moq_video_hint,
205}
206
207/// Optional catalog fields for [moq_video_init::hint].
208///
209/// Zero the struct and set only the `has_*` flags you want. Hints fill gaps the
210/// bitstream leaves (especially bitrate); a value the stream detects later wins
211/// for dimensions.
212#[repr(C)]
213#[allow(non_camel_case_types)]
214#[derive(Clone, Copy, Default)]
215pub struct moq_video_hint {
216	/// Encoded width in pixels when `has_coded` is true.
217	pub coded_width: u32,
218	/// Encoded height in pixels when `has_coded` is true.
219	pub coded_height: u32,
220	/// Whether `coded_width` and `coded_height` are present.
221	pub has_coded: bool,
222
223	/// Maximum bitrate in bits per second when `has_bitrate` is true.
224	pub bitrate: u64,
225	/// Whether `bitrate` is present.
226	pub has_bitrate: bool,
227
228	/// Frame rate when `has_framerate` is true.
229	pub framerate: f64,
230	/// Whether `framerate` is present.
231	pub has_framerate: bool,
232
233	/// Latency-optimized decode when `has_optimize_for_latency` is true.
234	pub optimize_for_latency: bool,
235	/// Whether `optimize_for_latency` is present.
236	pub has_optimize_for_latency: bool,
237}
238
239impl moq_video_hint {
240	/// The catalog hint these flags describe.
241	fn resolve(&self) -> moq_mux::catalog::VideoHint {
242		let mut out = moq_mux::catalog::VideoHint::default();
243		if self.has_coded {
244			out.coded_width = Some(self.coded_width);
245			out.coded_height = Some(self.coded_height);
246		}
247		if self.has_bitrate {
248			out.bitrate = Some(self.bitrate);
249		}
250		if self.has_framerate {
251			out.framerate = Some(self.framerate);
252		}
253		if self.has_optimize_for_latency {
254			out.optimize_for_latency = Some(self.optimize_for_latency);
255		}
256		out
257	}
258}
259
260/// Configuration for [moq_publish_container].
261///
262/// There is no label here: a container publishes and describes its own tracks,
263/// so a rendition name would have no single track to land on.
264#[repr(C)]
265#[allow(non_camel_case_types)]
266pub struct moq_container_init {
267	/// The container format, a [moq_container_format] value.
268	pub format: u32,
269
270	/// The leading chunk of the container, decoded immediately, or NULL.
271	pub init: *const u8,
272	/// Length of `init` in bytes.
273	pub init_len: usize,
274}
275
276/// Validate an audio format code from C.
277///
278/// The field is a `u32` rather than the enum: C can put any integer there, and matching an
279/// out-of-range discriminant as a Rust enum is UB. Same reason as [moq_audio_sample_format].
280fn audio_format_from_u32(value: u32) -> Result<moq_mux::import::AudioFormat, Error> {
281	use moq_mux::import::AudioFormat;
282	Ok(match value {
283		v if v == moq_audio_format::MOQ_AUDIO_FORMAT_AAC as u32 => AudioFormat::Aac,
284		v if v == moq_audio_format::MOQ_AUDIO_FORMAT_OPUS as u32 => AudioFormat::Opus,
285		v if v == moq_audio_format::MOQ_AUDIO_FORMAT_FLAC as u32 => AudioFormat::Flac,
286		v if v == moq_audio_format::MOQ_AUDIO_FORMAT_MP3 as u32 => AudioFormat::Mp3,
287		_ => return Err(Error::InvalidCode),
288	})
289}
290
291/// Validate a video format code from C. See [audio_format_from_u32].
292fn video_format_from_u32(value: u32) -> Result<moq_mux::import::VideoFormat, Error> {
293	use moq_mux::import::VideoFormat;
294	Ok(match value {
295		v if v == moq_video_format::MOQ_VIDEO_FORMAT_AVC1 as u32 => VideoFormat::Avc1,
296		v if v == moq_video_format::MOQ_VIDEO_FORMAT_AVC3 as u32 => VideoFormat::Avc3,
297		v if v == moq_video_format::MOQ_VIDEO_FORMAT_HVC1 as u32 => VideoFormat::Hvc1,
298		v if v == moq_video_format::MOQ_VIDEO_FORMAT_HEV1 as u32 => VideoFormat::Hev1,
299		v if v == moq_video_format::MOQ_VIDEO_FORMAT_AV01 as u32 => VideoFormat::Av01,
300		v if v == moq_video_format::MOQ_VIDEO_FORMAT_VP8 as u32 => VideoFormat::Vp8,
301		v if v == moq_video_format::MOQ_VIDEO_FORMAT_VP9 as u32 => VideoFormat::Vp9,
302		_ => return Err(Error::InvalidCode),
303	})
304}
305
306/// Validate a container format code from C. See [audio_format_from_u32].
307fn container_format_from_u32(value: u32) -> Result<moq_mux::import::ContainerFormat, Error> {
308	use moq_mux::import::ContainerFormat;
309	Ok(match value {
310		v if v == moq_container_format::MOQ_CONTAINER_FORMAT_FMP4 as u32 => ContainerFormat::Fmp4,
311		v if v == moq_container_format::MOQ_CONTAINER_FORMAT_MKV as u32 => ContainerFormat::Mkv,
312		v if v == moq_container_format::MOQ_CONTAINER_FORMAT_TS as u32 => ContainerFormat::Ts,
313		v if v == moq_container_format::MOQ_CONTAINER_FORMAT_FLV as u32 => ContainerFormat::Flv,
314		_ => return Err(Error::InvalidCode),
315	})
316}
317
318/// Information about a video rendition in the catalog.
319#[repr(C)]
320#[allow(non_camel_case_types)]
321pub struct moq_video_config {
322	/// The name of the track, NOT NULL terminated.
323	pub name: *const c_char,
324	pub name_len: usize,
325
326	/// The codec of the track, NOT NULL terminated
327	pub codec: *const c_char,
328	pub codec_len: usize,
329
330	/// The description of the track, or NULL if not used.
331	/// This is codec specific, for example H264:
332	///   - NULL: annex.b encoded
333	///   - Non-NULL: AVCC encoded
334	pub description: *const u8,
335	pub description_len: usize,
336
337	/// The encoded width/height of the media, a hint so a decoder can size its
338	/// buffers up front. Zero means absent, which no valid dimension is, so the
339	/// two are independent: a catalog carrying only one round-trips unchanged.
340	pub coded_width: u32,
341	pub coded_height: u32,
342
343	/// How the track's frames are wrapped.
344	pub container: moq_container,
345
346	/// Human-readable rendition name for track pickers, or NULL if not used.
347	pub label: *const c_char,
348	/// Length of `label` in bytes.
349	pub label_len: usize,
350}
351
352/// Catalog properties shared by every video rendition.
353///
354/// A false `has_*` flag clears that field from the next catalog rather than preserving its previous value.
355#[repr(C)]
356#[allow(non_camel_case_types)]
357#[derive(Clone, Copy, Default)]
358pub struct moq_video_properties {
359	/// Final rendered width in pixels when `has_display` is true.
360	pub display_width: u32,
361
362	/// Final rendered height in pixels when `has_display` is true.
363	pub display_height: u32,
364
365	/// Whether `display_width` and `display_height` are present.
366	pub has_display: bool,
367
368	/// Clockwise rotation in degrees when `has_rotation` is true.
369	pub rotation: f64,
370
371	/// Whether `rotation` is present.
372	pub has_rotation: bool,
373
374	/// Whether to flip horizontally after rotation when `has_flip` is true.
375	pub flip: bool,
376
377	/// Whether `flip` is present.
378	pub has_flip: bool,
379}
380
381/// Information about an audio rendition in the catalog.
382#[repr(C)]
383#[allow(non_camel_case_types)]
384pub struct moq_audio_config {
385	/// The name of the track, NOT NULL terminated
386	pub name: *const c_char,
387	pub name_len: usize,
388
389	/// The codec of the track, NOT NULL terminated
390	pub codec: *const c_char,
391	pub codec_len: usize,
392
393	/// The description of the track, or NULL if not used.
394	pub description: *const u8,
395	pub description_len: usize,
396
397	/// The sample rate of the track in Hz
398	pub sample_rate: u32,
399
400	/// The number of channels in the track
401	pub channel_count: u32,
402
403	/// How the track's frames are wrapped.
404	pub container: moq_container,
405
406	/// Human-readable rendition name for track pickers, or NULL if not used.
407	pub label: *const c_char,
408	/// Length of `label` in bytes.
409	pub label_len: usize,
410}
411
412/// Options for a JSON snapshot track (lossy latest-value mode).
413///
414/// The same config is passed to a producer and its consumers, but the consumer reads only
415/// `compression`; `delta_ratio` is producer-only.
416#[repr(C)]
417#[allow(non_camel_case_types)]
418pub struct moq_json_snapshot_config {
419	/// How aggressively the producer emits deltas instead of full snapshots. `0` disables deltas
420	/// (one snapshot per group); a positive value allows roughly that many snapshots' worth of
421	/// deltas before rolling. Ignored by the consumer.
422	pub delta_ratio: u32,
423
424	/// DEFLATE-compress each group. Must match on the producer and consumer.
425	pub compression: bool,
426}
427
428/// Options for a JSON stream track (lossless append-log mode).
429#[repr(C)]
430#[allow(non_camel_case_types)]
431pub struct moq_json_stream_config {
432	/// DEFLATE-compress the group. Must match on the producer and consumer.
433	pub compression: bool,
434}
435
436/// Options for a binary data track, in either mode.
437///
438/// The mode is fixed by which constructor is called ([moq_publish_binary_snapshot] or
439/// [moq_publish_binary_stream]), so it is not in here.
440#[repr(C)]
441#[allow(non_camel_case_types)]
442pub struct moq_binary_config {
443	/// DEFLATE-compress each payload, advertised in the catalog entry.
444	pub compression: bool,
445
446	/// The payloads' media type (e.g. `image/jpeg`), or NULL to leave it unstated.
447	pub mime: *const c_char,
448	/// Length of `mime` in bytes.
449	pub mime_len: usize,
450}
451
452/// A JSON value delivered by a consumer callback.
453#[repr(C)]
454#[allow(non_camel_case_types)]
455pub struct moq_json_value {
456	/// The JSON document as UTF-8, NOT NULL terminated.
457	pub json: *const c_char,
458	pub json_len: usize,
459}
460
461/// Information about a frame of media.
462#[repr(C)]
463#[allow(non_camel_case_types)]
464pub struct moq_frame {
465	/// The payload of the frame, or NULL/0 if the stream has ended
466	pub payload: *const u8,
467	pub payload_size: usize,
468
469	/// The presentation timestamp of the frame in microseconds
470	pub timestamp_us: u64,
471
472	/// Whether this frame opens a group or is a video keyframe; audio is true only at a group start.
473	pub keyframe: bool,
474}
475
476/// A best-effort raw track datagram delivered via [moq_consume_datagrams].
477#[repr(C)]
478#[allow(non_camel_case_types)]
479pub struct moq_datagram {
480	/// The payload of the datagram, or NULL/0 if the track has ended.
481	pub payload: *const u8,
482	pub payload_size: usize,
483
484	/// The presentation timestamp of the datagram in microseconds.
485	pub timestamp_us: u64,
486
487	/// Per-track sequence number, drawn from the same namespace as groups.
488	pub sequence: u64,
489}
490
491/// Publisher-side raw track properties.
492///
493/// A null [moq_publish_track] `info` pointer uses the moq-net defaults.
494/// A zero-initialized struct also uses those defaults, except `priority`, which
495/// has no presence flag: zero is the least urgent, and 127 is the moq-net default.
496#[repr(C)]
497#[allow(non_camel_case_types)]
498pub struct moq_track_info {
499	/// Priority, used to break ties between subscriptions of equal subscriber priority.
500	pub priority: u8,
501
502	/// Maximum age of a non-latest group before the publisher evicts it, in microseconds.
503	/// The publisher-side half of `moq_subscription.max_age_us`.
504	pub max_age_us: u64,
505	/// Whether `max_age_us` is set. When false, the publisher's default applies.
506	pub max_age_present: bool,
507
508	/// Per-frame timescale in ticks per second.
509	pub timescale: u64,
510	/// Whether `timescale` is set. When false, the default microsecond timescale
511	/// applies, matching the `timestamp_us` units used everywhere else in this ABI.
512	pub timescale_present: bool,
513}
514
515impl TryFrom<&moq_track_info> for moq_net::track::Info {
516	type Error = Error;
517
518	fn try_from(info: &moq_track_info) -> Result<Self, Self::Error> {
519		// Raw tracks default to a microsecond timescale, matching the C ABI's
520		// timestamp_us units. An explicit timescale below overrides it.
521		let mut out = moq_net::track::Info::default()
522			.with_timescale(moq_net::Timescale::MICRO)
523			.with_priority(info.priority);
524		if info.max_age_present {
525			out = out.with_max_age(std::time::Duration::from_micros(info.max_age_us));
526		}
527		if info.timescale_present {
528			out = out.with_timescale(moq_net::Timescale::new(info.timescale)?);
529		}
530		Ok(out)
531	}
532}
533
534/// Whether a published track has subscribers, as reported by a demand watcher.
535///
536/// The positive values an `on_demand` callback receives; `0` and negative codes are
537/// the terminal statuses every callback shares.
538#[repr(C)]
539#[allow(non_camel_case_types)]
540#[derive(Clone, Copy, Debug)]
541pub enum moq_demand {
542	/// At least one subscriber is active.
543	MOQ_DEMAND_USED = 1,
544	/// No subscriber is active.
545	MOQ_DEMAND_UNUSED = 2,
546}
547
548/// Subscriber-side raw track delivery preferences.
549///
550/// A null [moq_consume_track] or [moq_consume_track_update] `subscription`
551/// pointer uses the moq-net defaults.
552#[repr(C)]
553#[allow(non_camel_case_types)]
554pub struct moq_subscription {
555	/// Delivery priority. Higher values preempt lower ones under contention.
556	pub priority: u8,
557
558	/// Maximum age of a non-latest group before it is skipped, in microseconds.
559	/// Zero skips immediately. Enforced by the publisher's cache and by any local buffering.
560	pub max_age_us: u64,
561
562	/// The lowest group to deliver (a floor). A floor is not a request: `max_age_us` is
563	/// what asks for data, and delivery starts at the oldest group at or above the floor
564	/// within that budget (the latest group at the default budget of 0).
565	pub group_start: u64,
566	/// Whether `group_start` is present. When false, there is no floor.
567	pub group_start_present: bool,
568
569	/// First group not to deliver (exclusive), or ignored when `group_end_present` is
570	/// false. `0` is the empty range.
571	pub group_end: u64,
572	/// Whether `group_end` is present. When false, there is no end cap.
573	pub group_end_present: bool,
574}
575
576impl From<&moq_subscription> for moq_net::track::Subscription {
577	fn from(subscription: &moq_subscription) -> Self {
578		let mut out = moq_net::track::Subscription::default()
579			.with_priority(subscription.priority)
580			.with_max_age(std::time::Duration::from_micros(subscription.max_age_us));
581		if subscription.group_start_present {
582			out = out.with_start(moq_net::track::Position::group(subscription.group_start));
583		}
584		if subscription.group_end_present {
585			out = out.with_end(moq_net::track::Position::group(subscription.group_end));
586		}
587		out
588	}
589}
590
591/// A borrowed UTF-8 string slice, NOT NULL terminated.
592///
593/// Used in both directions. As an output (e.g. a JSON document libmoq hands back) the
594/// pointer borrows libmoq's own storage and is only valid until the owning resource is
595/// freed; see the function that fills it for the exact lifetime. As an input (e.g. a
596/// [moq_client_config] list) the pointer borrows the caller's storage and is only read
597/// during the call.
598#[repr(C)]
599#[allow(non_camel_case_types)]
600#[derive(Clone, Copy)]
601pub struct moq_string {
602	/// Pointer to `len` bytes of UTF-8, NOT NULL terminated.
603	pub data: *const c_char,
604	pub len: usize,
605}
606
607/// One untyped application catalog section: a name and its JSON value.
608///
609/// Both `name` and `json` are UTF-8, NOT NULL terminated, and borrow the catalog
610/// snapshot's storage. They stay valid until the snapshot is freed with
611/// [moq_consume_catalog_free]. `json` is the section's value serialized as JSON
612/// (parse it yourself); a top-level catalog key beyond `video`/`audio`.
613#[repr(C)]
614#[allow(non_camel_case_types)]
615pub struct moq_section {
616	/// The section name, NOT NULL terminated.
617	pub name: *const c_char,
618	pub name_len: usize,
619
620	/// The section value as a JSON document, NOT NULL terminated.
621	pub json: *const c_char,
622	pub json_len: usize,
623}
624
625/// A route advertisement: hops and costs.
626///
627/// Pair with [moq_publish_announce] or [moq_origin_dynamic]. Zeroed (NULL hops,
628/// hops_len 0, cost 0) is the default route. `hops` is borrowed for the duration
629/// of the call that reads it.
630///
631/// `cost` is the warm price: what pulling via this route costs today, lower
632/// wins. `cold` is the same path undiscounted; when `has_cold` is false it
633/// defaults to `cost`, which is what a publisher seeding its production cost
634/// wants. New fields always append, so a zeroed struct keeps meaning the
635/// defaults.
636#[repr(C)]
637#[allow(non_camel_case_types)]
638#[derive(Clone, Copy)]
639pub struct moq_route {
640	/// Hop ids, oldest first. NULL when `hops_len` is 0. 0 is the anonymous
641	/// mark and is legal on a received chain.
642	pub hops: *const u64,
643	pub hops_len: usize,
644	/// Preference among routes covering the same prefix: lower wins.
645	pub cost: u64,
646	/// The same path with every warm discount removed. Ignored unless `has_cold`.
647	pub cold: u64,
648	/// Whether `cold` applies. When false, `cold` defaults to `cost`.
649	pub has_cold: bool,
650}
651
652impl Default for moq_route {
653	fn default() -> Self {
654		Self {
655			hops: std::ptr::null(),
656			hops_len: 0,
657			cost: 0,
658			cold: 0,
659			has_cold: false,
660		}
661	}
662}
663
664/// Parse a [moq_route], treating NULL as the default.
665///
666/// An omitted `cold` (`has_cold` false) prices the route undiscounted, like a
667/// publisher seeding its production cost.
668///
669/// # Safety
670/// `route` may be NULL, or must point at a readable [moq_route] whose `hops`
671/// pointer is valid for `hops_len` elements.
672unsafe fn parse_route(route: *const moq_route) -> Result<moq_net::origin::Route, Error> {
673	let Some(route) = (unsafe { route.as_ref() }) else {
674		return Ok(moq_net::origin::Route::default());
675	};
676	let cold = if route.has_cold { route.cold } else { route.cost };
677	let mut route_hops = moq_net::Hops::new();
678	if route.hops_len > 0 {
679		if route.hops.is_null() {
680			return Err(Error::InvalidPointer);
681		}
682		let hops = unsafe { std::slice::from_raw_parts(route.hops, route.hops_len) };
683		for id in hops {
684			let hop = if *id == 0 {
685				moq_net::Hop::UNKNOWN
686			} else {
687				moq_net::Hop::new(*id).map_err(|e| Error::InvalidConfig(e.to_string()))?
688			};
689			route_hops.push(hop).map_err(|e| Error::InvalidConfig(e.to_string()))?;
690		}
691	}
692	Ok(moq_net::origin::Route::default()
693		.with_cost(moq_net::origin::Cost { warm: route.cost, cold })
694		.with_hops(route_hops))
695}
696
697/// A route announcement or retraction from an origin.
698#[repr(C)]
699#[allow(non_camel_case_types)]
700pub struct moq_announce_update {
701	/// The covered prefix, relative to the origin, NOT NULL terminated
702	pub prefix: *const c_char,
703	pub prefix_len: usize,
704
705	/// What each requested filter wildcard matched. Each string is NOT NULL terminated.
706	/// Meaningful only when `has_captures` is true; false means the route overlaps
707	/// the filter without pinning every wildcard.
708	pub captures: *const moq_string,
709	pub captures_len: usize,
710	pub has_captures: bool,
711
712	/// Whether the route is active or was retracted
713	/// This MUST toggle between true and false over the lifetime of the route
714	pub active: bool,
715}
716
717/// Statistics and protocol sampled from the same connection by [moq_session_snapshot].
718#[repr(C)]
719#[allow(non_camel_case_types)]
720pub struct moq_connection_snapshot {
721	/// Transport statistics, with per-metric availability flags.
722	pub stats: moq_connection_stats,
723	/// Negotiated draft name, backed by static storage valid for the process lifetime.
724	pub protocol: moq_string,
725}
726
727/// A snapshot of connection statistics, filled in by [moq_session_stats].
728///
729/// Each metric has a `*_valid` flag: when `false`, the matching value is meaningless because
730/// the transport backend doesn't report it (a `false` flag is NOT the same as a zero value).
731/// Native QUIC reports every metric; the browser WebTransport reports few or none. Initialize
732/// the struct to zero before the call; [moq_session_stats] overwrites every field.
733#[repr(C)]
734#[allow(non_camel_case_types)]
735pub struct moq_connection_stats {
736	/// Smoothed round-trip time, in microseconds.
737	pub rtt_us: u64,
738	pub rtt_valid: bool,
739
740	/// Estimated send bandwidth from the congestion controller, in bits per second.
741	pub estimated_send_rate_bps: u64,
742	pub estimated_send_rate_valid: bool,
743
744	/// Estimated receive bandwidth from MoQ PROBE, in bits per second.
745	pub estimated_recv_rate_bps: u64,
746	pub estimated_recv_rate_valid: bool,
747
748	/// Total bytes sent, including retransmissions and overhead.
749	pub bytes_sent: u64,
750	pub bytes_sent_valid: bool,
751
752	/// Total bytes received, including duplicates and overhead.
753	pub bytes_received: u64,
754	pub bytes_received_valid: bool,
755
756	/// Total bytes lost (detected via retransmission or acknowledgement).
757	pub bytes_lost: u64,
758	pub bytes_lost_valid: bool,
759
760	/// Total datagrams sent.
761	pub packets_sent: u64,
762	pub packets_sent_valid: bool,
763
764	/// Total datagrams received.
765	pub packets_received: u64,
766	pub packets_received_valid: bool,
767
768	/// Total datagrams detected as lost.
769	pub packets_lost: u64,
770	pub packets_lost_valid: bool,
771}
772
773impl From<&moq_net::session::Stats> for moq_connection_stats {
774	fn from(stats: &moq_net::session::Stats) -> Self {
775		// An Option<u64> becomes a (value, valid) pair; absent metrics report 0/false.
776		fn split(value: Option<u64>) -> (u64, bool) {
777			(value.unwrap_or(0), value.is_some())
778		}
779
780		let (rtt_us, rtt_valid) = split(stats.rtt.map(|d| d.as_micros() as u64));
781		let (estimated_send_rate_bps, estimated_send_rate_valid) =
782			split(stats.estimated_send_rate.map(moq_net::bandwidth::Rate::as_bps));
783		let (estimated_recv_rate_bps, estimated_recv_rate_valid) =
784			split(stats.estimated_recv_rate.map(moq_net::bandwidth::Rate::as_bps));
785		let (bytes_sent, bytes_sent_valid) = split(stats.bytes_sent);
786		let (bytes_received, bytes_received_valid) = split(stats.bytes_received);
787		let (bytes_lost, bytes_lost_valid) = split(stats.bytes_lost);
788		let (packets_sent, packets_sent_valid) = split(stats.packets_sent);
789		let (packets_received, packets_received_valid) = split(stats.packets_received);
790		let (packets_lost, packets_lost_valid) = split(stats.packets_lost);
791
792		Self {
793			rtt_us,
794			rtt_valid,
795			estimated_send_rate_bps,
796			estimated_send_rate_valid,
797			estimated_recv_rate_bps,
798			estimated_recv_rate_valid,
799			bytes_sent,
800			bytes_sent_valid,
801			bytes_received,
802			bytes_received_valid,
803			bytes_lost,
804			bytes_lost_valid,
805			packets_sent,
806			packets_sent_valid,
807			packets_received,
808			packets_received_valid,
809			packets_lost,
810			packets_lost_valid,
811		}
812	}
813}
814
815/// Initialize the library with a log level.
816///
817/// This should be called before any other functions.
818/// The log_level is a string: "error", "warn", "info", "debug", "trace"
819///
820/// Returns a zero on success, or a negative code on failure.
821///
822/// # Safety
823/// - The caller must ensure that level is a valid pointer to level_len bytes of data.
824#[unsafe(no_mangle)]
825pub unsafe extern "C" fn moq_log_level(level: *const c_char, level_len: usize) -> i32 {
826	ffi::enter(move || {
827		match unsafe { ffi::parse_str(level, level_len)? } {
828			"" => moq_tokio::Log::default(),
829			level => moq_tokio::Log::new(Level::from_str(level)?),
830		}
831		.init()?;
832
833		Ok(())
834	})
835}
836
837/// Human-readable reason for the most recent failed call on the calling thread.
838///
839/// libmoq functions return only a negative code; this exposes the matching message
840/// (including detail the code can't carry, e.g. which URL failed to parse or why a
841/// decode failed). The string is only meaningful after a call returned a negative
842/// code; check the code first.
843///
844/// Returns a NUL-terminated, UTF-8 pointer valid until the next libmoq call **on the
845/// same thread**, or NULL if no error has been recorded on this thread. Copy it if you
846/// need it to outlive the next call. Errors delivered through status callbacks carry
847/// their code directly; read this from inside the callback to get their reason.
848#[unsafe(no_mangle)]
849pub extern "C" fn moq_error() -> *const c_char {
850	ffi::last_error_ptr()
851}
852
853/// Structured protocol details for the most recent failed call on the calling thread.
854///
855/// When that failure was a session close or stream reset, writes the scope, verbatim
856/// wire code, and recognized kind into `out` and returns 0. Returns a negative code
857/// (and leaves `out` untouched) when the last error was not a protocol failure
858/// (transport, not-found, a bad handle, ...). Do not parse [moq_error] for this.
859///
860/// The values are only meaningful after a call returned a negative code; check the
861/// code first. Same lifetime as [moq_error]: overwritten by the next libmoq call on
862/// this thread. Errors delivered through status callbacks are recorded before the
863/// callback runs, so read this from inside the callback.
864///
865/// # Safety
866/// - The caller must ensure that `out` is a valid pointer to a [moq_protocol_error].
867#[unsafe(no_mangle)]
868pub unsafe extern "C" fn moq_error_protocol(out: *mut moq_protocol_error) -> i32 {
869	// Do not go through `enter`: a miss must not overwrite the last error we are inspecting.
870	if out.is_null() {
871		return Error::InvalidPointer.code();
872	}
873	if ffi::last_protocol(unsafe { &mut *out }) {
874		0
875	} else {
876		Error::NotFound.code()
877	}
878}
879
880/// The protocol version names this build offers by default, spelled the way
881/// [moq_client_config]'s `versions` expects. Built once; the slices are valid for the life of
882/// the process.
883static VERSION_NAMES: std::sync::LazyLock<Vec<String>> =
884	std::sync::LazyLock::new(|| moq_net::Versions::all().iter().map(|v| v.to_string()).collect());
885
886/// List the protocol versions offered during the handshake by default.
887///
888/// Writes up to `count` names into `dst` and returns the total number available, which
889/// may be larger than `count`. Pass a NULL `dst` with a zero `count` to size the array
890/// first. Each name borrows a static string valid for the life of the process, so a
891/// caller building a menu can hold them indefinitely.
892///
893/// Work-in-progress versions are omitted, since they are not advertised unless pinned;
894/// a dial still accepts them by name.
895///
896/// Returns the total count on success, or a negative code on failure.
897///
898/// # Safety
899/// - The caller must ensure that `dst` is either NULL with a zero `count`, or a valid
900///   pointer to `count` writable [moq_string] values.
901#[unsafe(no_mangle)]
902pub unsafe extern "C" fn moq_versions(dst: *mut moq_string, count: usize) -> i32 {
903	ffi::enter(move || {
904		if !dst.is_null() {
905			let dst = unsafe { std::slice::from_raw_parts_mut(dst, count) };
906			for (slot, name) in dst.iter_mut().zip(VERSION_NAMES.iter()) {
907				slot.data = name.as_ptr().cast::<c_char>();
908				slot.len = name.len();
909			}
910		} else if count != 0 {
911			return Err(Error::InvalidPointer);
912		}
913
914		Ok(VERSION_NAMES.len())
915	})
916}
917
918/// Whether this build can capture qlog traces.
919///
920/// Capture is compile-time optional. [moq_client_config]'s `quic_qlog` accepts a directory
921/// either way, but dialing fails when the support is absent, so a caller offering the
922/// knob should hide it rather than surface an option that cannot work.
923#[unsafe(no_mangle)]
924pub extern "C" fn moq_qlog_supported() -> bool {
925	moq_tokio::qlog_supported()
926}
927
928/// A duration as microseconds, saturating rather than wrapping.
929fn micros(duration: std::time::Duration) -> u64 {
930	duration.as_micros().min(u64::MAX as u128) as u64
931}
932
933/// Settings for [moq_session_connect], or NULL to dial with the defaults.
934///
935/// Zero it (`memset`, or a `{0}` initializer) and set only what you need: a
936/// zeroed struct means the defaults throughout. That is why the knobs whose
937/// default is not zero carry a `has_*` flag rather than being read directly. The
938/// WebSocket fallback is on by default and the reconnect backoff starts at one
939/// second, so a caller who never touched them would otherwise silently turn them
940/// off.
941///
942/// New settings are appended to the end of this struct, and a zeroed one keeps
943/// the previous behavior, so adding one does not disturb existing callers.
944#[repr(C)]
945#[allow(non_camel_case_types)]
946pub struct moq_client_config {
947	/// Protocol versions to offer during the handshake, most preferred first.
948	/// NULL/0 offers everything this build supports. Names are spelled the way
949	/// the CLI spells them (`moq-lite-05`, `moq-transport-22`); [moq_versions]
950	/// lists what is on offer.
951	pub versions: *const moq_string,
952	pub versions_len: usize,
953
954	/// Local socket address to bind, or NULL for the wildcard address.
955	pub bind: *const c_char,
956	pub bind_len: usize,
957
958	/// How long a dial may take before it gives up.
959	pub connect_timeout_us: u64,
960	pub has_connect_timeout: bool,
961
962	/// Happy Eyeballs: how long before the next address is also dialed.
963	pub failover_delay_us: u64,
964	pub has_failover_delay: bool,
965
966	/// Happy Eyeballs: how long the first family waits for the AAAA answer.
967	pub resolution_delay_us: u64,
968	pub has_resolution_delay: bool,
969
970	/// Whether the WebSocket fallback may be raced, for a UDP-blocked network.
971	/// Enabled unless you turn it off, hence the flag.
972	pub websocket_enabled: bool,
973	pub has_websocket_enabled: bool,
974
975	/// How long QUIC gets before the WebSocket fallback is also dialed.
976	pub websocket_delay_us: u64,
977	pub has_websocket_delay: bool,
978
979	/// Accept any certificate. Development only: prefer `tls_fingerprints`,
980	/// and pairing this with a fingerprint or a root is rejected at dial.
981	pub tls_disable_verify: bool,
982
983	/// Whether to trust the platform root store. Its default depends on the
984	/// backend, so it needs the flag to distinguish "off" from "unset".
985	pub tls_system_roots: bool,
986	pub has_tls_system_roots: bool,
987
988	/// Extra root certificate paths to trust.
989	pub tls_roots: *const moq_string,
990	pub tls_roots_len: usize,
991
992	/// SHA-256 certificate fingerprints to pin, hex encoded. The native
993	/// equivalent of the browser's `serverCertificateHashes`.
994	pub tls_fingerprints: *const moq_string,
995	pub tls_fingerprints_len: usize,
996
997	/// SNI override, or NULL to use the host from the URL.
998	pub tls_host_name: *const c_char,
999	pub tls_host_name_len: usize,
1000
1001	/// Client certificate and key paths for mTLS, or NULL for none.
1002	pub tls_cert: *const c_char,
1003	pub tls_cert_len: usize,
1004	pub tls_key: *const c_char,
1005	pub tls_key_len: usize,
1006
1007	/// Reconnect pacing. Each must leave a non-zero delay or retrying would
1008	/// spin, which is rejected at dial.
1009	pub backoff_initial_us: u64,
1010	pub has_backoff_initial: bool,
1011	pub backoff_multiplier: u32,
1012	pub has_backoff_multiplier: bool,
1013	pub backoff_max_us: u64,
1014	pub has_backoff_max: bool,
1015	/// How long reconnection keeps trying before giving up for good.
1016	pub backoff_timeout_us: u64,
1017	pub has_backoff_timeout: bool,
1018
1019	/// QUIC transport tuning, all ignored by the WebSocket fallback.
1020	pub quic_max_streams: u64,
1021	pub has_quic_max_streams: bool,
1022	pub quic_idle_timeout_us: u64,
1023	pub has_quic_idle_timeout: bool,
1024	pub quic_keep_alive_us: u64,
1025	pub has_quic_keep_alive: bool,
1026	/// Generic segmentation offload and path MTU discovery. Both default to the
1027	/// backend's choice, so both need their flag.
1028	pub quic_gso: bool,
1029	pub has_quic_gso: bool,
1030	pub quic_mtu_discovery: bool,
1031	pub has_quic_mtu_discovery: bool,
1032
1033	/// Congestion control family name, or NULL for the backend's choice.
1034	pub quic_congestion_control: *const c_char,
1035	pub quic_congestion_control_len: usize,
1036
1037	/// Directory to write qlog traces into, or NULL for none. Capture is
1038	/// compile-time optional; see [moq_qlog_supported].
1039	pub quic_qlog: *const c_char,
1040	pub quic_qlog_len: usize,
1041}
1042
1043/// The settings [moq_session_connect] dials with when given NULL.
1044///
1045/// Behaviorally the same as a zeroed struct, so this is for display rather than
1046/// for dialing: a settings UI can show the real numbers instead of hardcoding
1047/// ones that go stale when a default is retuned. The knobs whose default depends
1048/// on the backend (GSO, path MTU discovery, congestion control, the TLS root
1049/// store) come back with their `has_*` flag false, since there is no single value
1050/// to report.
1051///
1052/// Returned by value because there is nothing to fail: no handle to look up and
1053/// no pointer to reject. Prefer a zeroed struct when you only mean to set a knob
1054/// or two, and this when you want to read the numbers.
1055#[unsafe(no_mangle)]
1056pub extern "C" fn moq_client_defaults() -> moq_client_config {
1057	// SAFETY: every field is a scalar or a raw pointer, so all-zero is a valid
1058	// value, and it is the one that means "unset" throughout.
1059	let mut dst: moq_client_config = unsafe { std::mem::zeroed() };
1060
1061	// A panic here would have no way to report itself, so fall back to the zeroed
1062	// struct: it is what "the defaults" means to a dial anyway, and only the
1063	// reported numbers would be wrong.
1064	let filled = std::panic::catch_unwind(|| {
1065		let mut dst: moq_client_config = unsafe { std::mem::zeroed() };
1066		let config = crate::client::Config::default();
1067
1068		let connect = config.connect.resolve();
1069		dst.connect_timeout_us = micros(connect.timeout);
1070		dst.has_connect_timeout = true;
1071		dst.failover_delay_us = micros(connect.race);
1072		dst.has_failover_delay = true;
1073		dst.resolution_delay_us = micros(connect.resolution_delay);
1074		dst.has_resolution_delay = true;
1075
1076		let websocket = config.connect.websocket.resolve();
1077		dst.websocket_enabled = websocket.enabled;
1078		dst.has_websocket_enabled = true;
1079		dst.websocket_delay_us = micros(websocket.delay);
1080		dst.has_websocket_delay = true;
1081
1082		dst.backoff_initial_us = micros(config.connect.backoff.initial);
1083		dst.has_backoff_initial = true;
1084		dst.backoff_multiplier = config.connect.backoff.multiplier;
1085		dst.has_backoff_multiplier = true;
1086		dst.backoff_max_us = micros(config.connect.backoff.max);
1087		dst.has_backoff_max = true;
1088		dst.backoff_timeout_us = micros(config.connect.backoff.timeout);
1089		dst.has_backoff_timeout = true;
1090
1091		let quic = config.quic.resolve();
1092		dst.quic_max_streams = quic.max_streams;
1093		dst.has_quic_max_streams = true;
1094		dst.quic_idle_timeout_us = micros(quic.idle_timeout);
1095		dst.has_quic_idle_timeout = true;
1096		if let Some(keep_alive) = quic.keep_alive {
1097			dst.quic_keep_alive_us = micros(keep_alive);
1098			dst.has_quic_keep_alive = true;
1099		}
1100
1101		dst
1102	});
1103
1104	if let Ok(value) = filled {
1105		dst = value;
1106	}
1107
1108	dst
1109}
1110
1111/// Resolve handles under the global lock, prepare the client without it, then insert
1112/// the ready session under a short second lock.
1113unsafe fn connect_session(
1114	url: *const c_char,
1115	url_len: usize,
1116	config: *const moq_client_config,
1117	origin_publish: u32,
1118	origin_consume: u32,
1119	on_status: ffi::moq_status_callback,
1120	user_data: *mut c_void,
1121) -> Result<crate::Id, Error> {
1122	let url = ffi::parse_url(url, url_len)?;
1123	let origin_publish = ffi::parse_id_optional(origin_publish)?;
1124	let origin_consume = ffi::parse_id_optional(origin_consume)?;
1125
1126	// Parse before taking the lock: it validates, and a rejected value should not
1127	// have blocked every other call while it was being read.
1128	let config = unsafe { crate::parse_client(config.as_ref())? };
1129
1130	let (publish, consume) = {
1131		let state = State::lock();
1132		let publish = origin_publish.map(|id| state.origin.get(id)).transpose()?.cloned();
1133		let consume = origin_consume.map(|id| state.origin.get(id)).transpose()?.cloned();
1134		(publish, consume)
1135	};
1136
1137	let callback = unsafe { ffi::OnStatus::new(user_data, on_status)? };
1138	let request = Connect {
1139		config,
1140		url,
1141		publish,
1142		consume,
1143		callback,
1144	}
1145	.prepare()?;
1146
1147	State::lock().session.connect(request)
1148}
1149
1150/// Start establishing a connection to a MoQ server.
1151///
1152/// Takes origin handles, which are used for publishing and consuming broadcasts respectively.
1153/// - Any broadcasts in `origin_publish` will be announced to the server.
1154/// - Any broadcasts announced by the server will be available in `origin_consume`.
1155/// - If an origin handle is 0, that functionality is completely disabled.
1156///
1157/// This may be called multiple times to connect to different servers.
1158/// Origins can be shared across sessions, useful for fanout or relaying.
1159///
1160/// Pass NULL for `config` to dial with the defaults. Fill in a
1161/// [moq_client_config] to pin a protocol version, adjust TLS trust, or tune the
1162/// transport; it is read during the call and not retained, so the same one can
1163/// dial any number of sessions.
1164///
1165/// Returns a non-zero handle to the session on success, or a negative code on (immediate) failure.
1166/// You should call [moq_session_close], even on error, to free up resources.
1167///
1168/// The session reconnects automatically with exponential backoff if the connection drops.
1169/// Published broadcasts are re-announced and consumers re-subscribed on each reconnect,
1170/// since the origins outlive the underlying connection.
1171///
1172/// `on_status` reports the session lifecycle through its status code:
1173/// - `> 0` on every (re)connect, carrying the connection epoch (`1` = first connect,
1174///   `2` = first reconnect, and so on), so a reconnect is distinguishable from the
1175///   initial connect. May fire repeatedly. Transient disconnects are not reported.
1176/// - `0` when the session is closed cleanly via [moq_session_close] (terminal).
1177/// - a negative error code if reconnection permanently gives up, e.g. the backoff
1178///   timeout is exceeded (terminal).
1179///
1180/// After a terminal (`<= 0`) status, `on_status` is never called again and `user_data`
1181/// is never touched again, so that final callback is the point to release `user_data`.
1182/// The terminal `0` fires even after [moq_session_close], so do not free `user_data` on
1183/// the close call itself.
1184///
1185/// # Safety
1186/// - The caller must ensure that url is a valid pointer to url_len bytes of data.
1187/// - `config` must be NULL, or an aligned, readable [moq_client_config]. Every
1188///   non-NULL pointer inside it must be valid for its paired length, and all of
1189///   them must stay alive for the duration of this call: the config is read
1190///   here, not copied by whoever filled it in.
1191/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_status` callback.
1192#[unsafe(no_mangle)]
1193pub unsafe extern "C" fn moq_session_connect(
1194	url: *const c_char,
1195	url_len: usize,
1196	config: *const moq_client_config,
1197	origin_publish: u32,
1198	origin_consume: u32,
1199	on_status: ffi::moq_status_callback,
1200	user_data: *mut c_void,
1201) -> i32 {
1202	ffi::enter(move || unsafe {
1203		connect_session(
1204			url,
1205			url_len,
1206			config,
1207			origin_publish,
1208			origin_consume,
1209			on_status,
1210			user_data,
1211		)
1212	})
1213}
1214
1215/// Request that a session shut down.
1216///
1217/// Returns immediately: zero on success, or a negative code if the session is
1218/// unknown or already closing. Does NOT free `user_data`. The
1219/// [moq_session_connect] `on_status` callback still fires once more with a
1220/// terminal `0` (or a negative error), and that final callback is where
1221/// `user_data` should be released. Safe to call from any thread, including from
1222/// within `on_status`.
1223#[unsafe(no_mangle)]
1224pub extern "C" fn moq_session_close(session: u32) -> i32 {
1225	ffi::enter(move || {
1226		let session = ffi::parse_id(session)?;
1227		State::lock().session.close(session)
1228	})
1229}
1230
1231/// Snapshot the current connection statistics for a session.
1232///
1233/// Fills `dst` with a point-in-time view of the underlying QUIC/WebTransport connection
1234/// (RTT, bandwidth estimates, byte/packet counters). Each metric carries a `*_valid` flag
1235/// since availability depends on the transport backend; see [moq_connection_stats].
1236///
1237/// Returns zero on success, or a negative code on failure: the session handle is unknown, or
1238/// the session is currently reconnecting and has no live connection (in which case `dst` is
1239/// left untouched). Safe to call repeatedly to poll stats over the life of the session.
1240///
1241/// # Safety
1242/// - The caller must ensure that `dst` is a valid pointer to a [moq_connection_stats] struct.
1243#[unsafe(no_mangle)]
1244pub unsafe extern "C" fn moq_session_stats(session: u32, dst: *mut moq_connection_stats) -> i32 {
1245	ffi::enter(move || {
1246		let session = ffi::parse_id(session)?;
1247		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1248		let stats = State::lock().session.stats(session)?;
1249		*dst = moq_connection_stats::from(&stats);
1250		Ok(())
1251	})
1252}
1253
1254/// Snapshot statistics and the negotiated protocol from the same live connection.
1255///
1256/// Returns zero on success, or a negative code when the handle is unknown or offline
1257/// between reconnects. On failure, `dst` is untouched. The protocol string points at
1258/// static storage valid for the process lifetime and must not be freed.
1259///
1260/// # Safety
1261/// - `dst` must point at a writable [moq_connection_snapshot] struct.
1262#[unsafe(no_mangle)]
1263pub unsafe extern "C" fn moq_session_snapshot(session: u32, dst: *mut moq_connection_snapshot) -> i32 {
1264	ffi::enter(move || {
1265		let session = ffi::parse_id(session)?;
1266		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1267		let (stats, version) = State::lock().session.snapshot(session)?;
1268		let name = version.as_str();
1269		*dst = moq_connection_snapshot {
1270			stats: moq_connection_stats::from(&stats),
1271			protocol: moq_string {
1272				data: name.as_ptr().cast::<c_char>(),
1273				len: name.len(),
1274			},
1275		};
1276		Ok(())
1277	})
1278}
1279
1280/// Settings for [moq_server_listen].
1281///
1282/// Zero it and set only what you need; new settings are appended, and a zeroed
1283/// one keeps the previous behavior. TLS is required: set `tls_cert` and `tls_key`,
1284/// or `tls_generate`.
1285#[repr(C)]
1286#[allow(non_camel_case_types)]
1287pub struct moq_server_config {
1288	/// Address to bind, e.g. `[::]:443`, `127.0.0.1:0`, or `localhost:4443`; NULL
1289	/// for `[::]:443`. A port of 0 picks one; read it back with [moq_server_addr].
1290	pub bind: *const c_char,
1291	pub bind_len: usize,
1292
1293	/// Certificate chain paths (PEM), paired with `tls_key`.
1294	pub tls_cert: *const moq_string,
1295	pub tls_cert_len: usize,
1296
1297	/// Private key paths (PEM), paired with `tls_cert`.
1298	pub tls_key: *const moq_string,
1299	pub tls_key_len: usize,
1300
1301	/// Hostnames to generate a self-signed certificate for. Clients must pin its
1302	/// fingerprint ([moq_server_fingerprints]) or disable verification.
1303	pub tls_generate: *const moq_string,
1304	pub tls_generate_len: usize,
1305}
1306
1307/// Build the listener `config` describes, binding its socket.
1308///
1309/// # Safety
1310/// - Every non-NULL pointer in `config` must be valid for its length.
1311unsafe fn parse_server(config: &moq_server_config) -> Result<moq_tokio::Server, Error> {
1312	let mut listen = moq_tokio::listen::Config::default();
1313	if let Some(bind) = unsafe { ffi::parse_str_optional(config.bind, config.bind_len)? } {
1314		let bind = moq_tokio::listen::Bind::from_str(bind)
1315			.map_err(|_| Error::InvalidConfig(format!("invalid bind address: {bind}")))?;
1316		listen.bind = Some(bind);
1317	}
1318	listen.tls.cert = unsafe { ffi::parse_strings(config.tls_cert, config.tls_cert_len)? }
1319		.into_iter()
1320		.map(Into::into)
1321		.collect();
1322	listen.tls.key = unsafe { ffi::parse_strings(config.tls_key, config.tls_key_len)? }
1323		.into_iter()
1324		.map(Into::into)
1325		.collect();
1326	listen.tls.generate = unsafe { ffi::parse_strings(config.tls_generate, config.tls_generate_len)? };
1327
1328	listen
1329		.init(Default::default())
1330		.map_err(|err| Error::InvalidConfig(err.to_string()))
1331}
1332
1333/// Listen for incoming sessions.
1334///
1335/// Binds before returning, so a bad address or certificate fails here with a reason in
1336/// [moq_error]. Returns a non-zero server handle on success, or a negative code on failure.
1337///
1338/// `on_request` is called with a positive session request handle for each incoming
1339/// session, then exactly once more with a terminal code: `0` (stopped cleanly, including
1340/// after [moq_server_close]) or a negative error. After the terminal (`<= 0`) callback,
1341/// `user_data` is never touched again, so release it there. Answer each request with
1342/// [moq_session_request_accept], [moq_session_request_reject], or [moq_session_request_free].
1343///
1344/// # Safety
1345/// - `config` must point at a readable [moq_server_config] whose non-NULL pointers are
1346///   valid for their paired lengths during this call.
1347/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_request` callback.
1348#[unsafe(no_mangle)]
1349pub unsafe extern "C" fn moq_server_listen(
1350	config: *const moq_server_config,
1351	on_request: ffi::moq_status_callback,
1352	user_data: *mut c_void,
1353) -> i32 {
1354	ffi::enter(move || {
1355		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1356		let on_request = unsafe { ffi::OnStatus::new(user_data, on_request)? };
1357		// Bind without the global lock: resolving and loading certificates can be slow.
1358		let server = unsafe { parse_server(config)? };
1359		State::lock().server.listen(server, on_request)
1360	})
1361}
1362
1363/// The address a server bound, e.g. `127.0.0.1:4443`.
1364///
1365/// The destination borrows the server's storage, valid until its terminal
1366/// `on_request` callback. Returns a zero on success, or a negative code on failure.
1367///
1368/// # Safety
1369/// - `dst` must point at a writable [moq_string].
1370#[unsafe(no_mangle)]
1371pub unsafe extern "C" fn moq_server_addr(server: u32, dst: *mut moq_string) -> i32 {
1372	ffi::enter(move || {
1373		let server = ffi::parse_id(server)?;
1374		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1375		State::lock().server.addr(server, dst)
1376	})
1377}
1378
1379/// The SHA-256 fingerprints of the server's certificates, hex encoded.
1380///
1381/// Pin these on a client (`tls_fingerprints` in [moq_client_config], or a browser's
1382/// `serverCertificateHashes`) to trust a generated certificate. Writes up to `count`
1383/// into `dst` and returns the total number available; pass a NULL `dst` with a zero
1384/// `count` to size the array first. Each string borrows the server's storage, valid
1385/// until its terminal `on_request` callback.
1386///
1387/// Returns the total count on success, or a negative code on failure.
1388///
1389/// # Safety
1390/// - `dst` must be NULL with a zero `count`, or point to `count` writable [moq_string] values.
1391#[unsafe(no_mangle)]
1392pub unsafe extern "C" fn moq_server_fingerprints(server: u32, dst: *mut moq_string, count: usize) -> i32 {
1393	ffi::enter(move || {
1394		let server = ffi::parse_id(server)?;
1395		let dst = if count == 0 {
1396			&mut [][..]
1397		} else {
1398			if dst.is_null() {
1399				return Err(Error::InvalidPointer);
1400			}
1401			unsafe { std::slice::from_raw_parts_mut(dst, count) }
1402		};
1403		State::lock().server.fingerprints(server, dst)
1404	})
1405}
1406
1407/// Stop listening.
1408///
1409/// Returns immediately: zero on success, or a negative code if the server is unknown or
1410/// already closing. Sessions already accepted keep running. The `on_request` callback
1411/// still fires once more with a terminal `0` after the sockets are released, so the
1412/// address can be bound again from there; release `user_data` in that callback.
1413#[unsafe(no_mangle)]
1414pub extern "C" fn moq_server_close(server: u32) -> i32 {
1415	ffi::enter(move || {
1416		let server = ffi::parse_id(server)?;
1417		State::lock().server.close(server)
1418	})
1419}
1420
1421/// The path of a session request, without the query, or empty for the root.
1422///
1423/// The destination borrows the request's storage: copy it out before accept,
1424/// reject, or [moq_session_request_free]. Returns a zero on success, or a negative
1425/// code on failure.
1426///
1427/// # Safety
1428/// - `dst` must point at a writable [moq_string].
1429#[unsafe(no_mangle)]
1430pub unsafe extern "C" fn moq_session_request_path(request: u32, dst: *mut moq_string) -> i32 {
1431	ffi::enter(move || {
1432		let request = ffi::parse_id(request)?;
1433		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1434		State::lock().server.request_path(request, dst)
1435	})
1436}
1437
1438/// The query of a session request without the leading `?`, or a NULL `data` if it has none.
1439///
1440/// Where a token usually rides. The destination borrows the request's storage, like
1441/// [moq_session_request_path]. Returns a zero on success, or a negative code on failure.
1442///
1443/// # Safety
1444/// - `dst` must point at a writable [moq_string].
1445#[unsafe(no_mangle)]
1446pub unsafe extern "C" fn moq_session_request_query(request: u32, dst: *mut moq_string) -> i32 {
1447	ffi::enter(move || {
1448		let request = ffi::parse_id(request)?;
1449		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1450		State::lock().server.request_query(request, dst)
1451	})
1452}
1453
1454/// Accept a session request, completing the MoQ handshake.
1455///
1456/// Takes origin handles like [moq_session_connect]: broadcasts in `origin_publish` are
1457/// announced to the peer, and broadcasts the peer announces land in `origin_consume`.
1458/// An origin handle of 0 disables that direction.
1459///
1460/// Consumes the request handle on success and returns a non-zero session handle, or a
1461/// negative code on failure (leaving the request unanswered). The session works with every
1462/// `moq_session_*` call; stats and bandwidth report offline until the handshake completes.
1463///
1464/// `on_status` reports the session lifecycle:
1465/// - `1` once the handshake completes. An accepted session is a single connection, so it
1466///   never reconnects and never reports more.
1467/// - `0` when closed via [moq_session_close] (terminal).
1468/// - a negative error code if the handshake fails or the peer closes the session; read
1469///   [moq_error_protocol] for the peer's close code (terminal).
1470///
1471/// # Safety
1472/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_status` callback.
1473#[unsafe(no_mangle)]
1474pub unsafe extern "C" fn moq_session_request_accept(
1475	request: u32,
1476	origin_publish: u32,
1477	origin_consume: u32,
1478	on_status: ffi::moq_status_callback,
1479	user_data: *mut c_void,
1480) -> i32 {
1481	ffi::enter(move || {
1482		let request = ffi::parse_id(request)?;
1483		let origin_publish = ffi::parse_id_optional(origin_publish)?;
1484		let origin_consume = ffi::parse_id_optional(origin_consume)?;
1485		let callback = unsafe { ffi::OnStatus::new(user_data, on_status)? };
1486
1487		let mut state = State::lock();
1488		let publish = origin_publish.map(|id| state.origin.get(id)).transpose()?.cloned();
1489		let consume = origin_consume.map(|id| state.origin.get(id)).transpose()?.cloned();
1490		let request = state.server.request_take(request)?;
1491		state.session.accept(request, publish, consume, callback)
1492	})
1493}
1494
1495/// Reject a session request with an HTTP-style status code.
1496///
1497/// 401 and 403 are sent as the protocol's unauthorized close; every other code is sent
1498/// as an application error. Consumes the request handle. Returns a zero on success, or
1499/// a negative code on failure.
1500#[unsafe(no_mangle)]
1501pub extern "C" fn moq_session_request_reject(request: u32, code: u16) -> i32 {
1502	ffi::enter(move || {
1503		let request = ffi::parse_id(request)?;
1504		let request = State::lock().server.request_take(request)?;
1505		let reject = match code {
1506			401 => moq_tokio::server::Reject::Unauthorized,
1507			403 => moq_tokio::server::Reject::Forbidden,
1508			code => moq_tokio::server::Reject::App(code),
1509		};
1510		// Rejecting only queues the close, so this never waits on the network.
1511		Ok::<_, Error>(pollster::block_on(request.reject(reject))?)
1512	})
1513}
1514
1515/// Free a session request without accepting or rejecting it.
1516///
1517/// Dropping the request closes the session. Returns a zero on success, or a negative
1518/// code if the handle is unknown.
1519#[unsafe(no_mangle)]
1520pub extern "C" fn moq_session_request_free(request: u32) -> i32 {
1521	ffi::enter(move || {
1522		let request = ffi::parse_id(request)?;
1523		State::lock().server.request_take(request)?;
1524		Ok(())
1525	})
1526}
1527
1528/// Create an origin for publishing broadcasts.
1529///
1530/// Origins contain any number of broadcasts addressed by path.
1531/// The same broadcast can be published to multiple origins under different paths.
1532///
1533/// [moq_origin_announced] can be used to discover broadcasts published to this origin.
1534/// This is extremely useful for discovering what is available on the server to [moq_origin_request].
1535///
1536/// Returns a non-zero handle to the origin on success.
1537#[unsafe(no_mangle)]
1538pub extern "C" fn moq_origin_create() -> i32 {
1539	ffi::enter(move || State::lock().origin.create())
1540}
1541
1542/// Create a broadcast at `path` on an origin, for publishing media tracks.
1543///
1544/// The broadcast is invisible and unroutable, on this origin and its peers
1545/// alike, until [moq_publish_announce]. Fill it with the `moq_publish_*`
1546/// functions, then announce it. [moq_publish_close] ends it for good.
1547///
1548/// Returns a non-zero broadcast handle on success, or a negative code on failure.
1549///
1550/// # Safety
1551/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
1552#[unsafe(no_mangle)]
1553pub unsafe extern "C" fn moq_origin_create_broadcast(origin: u32, path: *const c_char, path_len: usize) -> i32 {
1554	ffi::enter(move || {
1555		let origin = ffi::parse_id(origin)?;
1556		let path = unsafe { ffi::parse_str(path, path_len)? };
1557
1558		let mut state = State::lock();
1559		let broadcast = state.origin.create_broadcast(origin, path)?;
1560		state.publish.create(broadcast)
1561	})
1562}
1563
1564/// Advertise `prefix` and serve the requests beneath it.
1565///
1566/// A route claims `prefix` and every path beneath it (the empty prefix claims
1567/// every path). A service that only serves some of them advertises the
1568/// covering prefix and rejects the rest as they are requested. `on_request` is
1569/// required: a NULL callback is refused before the route is advertised. It is
1570/// invoked with a positive request handle for each
1571/// pending broadcast, then exactly once more with a terminal code: `0` (stopped
1572/// cleanly, including after [moq_origin_dynamic_cancel]) or a negative error.
1573/// After the terminal (`<= 0`) callback, `user_data` is never touched again.
1574///
1575/// Returns a non-zero handle on success, or a negative code on failure.
1576///
1577/// # Safety
1578/// - The caller must ensure that prefix is a valid pointer to prefix_len bytes of data.
1579/// - `route` may be NULL, or must point at a readable [moq_route].
1580/// - `on_request` must be non-NULL; a missing callback is refused before the route is advertised.
1581/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_request` callback.
1582#[unsafe(no_mangle)]
1583pub unsafe extern "C" fn moq_origin_dynamic(
1584	origin: u32,
1585	prefix: *const c_char,
1586	prefix_len: usize,
1587	route: *const moq_route,
1588	on_request: ffi::moq_status_callback,
1589	user_data: *mut c_void,
1590) -> i32 {
1591	ffi::enter(move || {
1592		let origin = ffi::parse_id(origin)?;
1593		let prefix = unsafe { ffi::parse_str(prefix, prefix_len)? };
1594		let route = unsafe { parse_route(route)? };
1595		let on_request = unsafe { ffi::OnStatus::new(user_data, on_request)? };
1596		State::lock().origin.dynamic(origin, prefix, route, on_request)
1597	})
1598}
1599
1600/// Re-price a served route in place. The prefix cannot change.
1601///
1602/// Returns a zero on success, or a negative code on failure.
1603///
1604/// # Safety
1605/// - `route` may be NULL, or must point at a readable [moq_route].
1606#[unsafe(no_mangle)]
1607pub unsafe extern "C" fn moq_origin_dynamic_update(dynamic: u32, route: *const moq_route) -> i32 {
1608	ffi::enter(move || {
1609		let dynamic = ffi::parse_id(dynamic)?;
1610		let route = unsafe { parse_route(route)? };
1611		State::lock().origin.dynamic_update(dynamic, route)
1612	})
1613}
1614
1615/// Stop serving and retract the route.
1616///
1617/// Returns immediately: zero on success, or a negative code if already closed.
1618/// The [moq_origin_dynamic] `on_request` callback still fires once more with a
1619/// terminal `0` (or a negative error), and that final callback is where
1620/// `user_data` should be released.
1621#[unsafe(no_mangle)]
1622pub extern "C" fn moq_origin_dynamic_cancel(dynamic: u32) -> i32 {
1623	ffi::enter(move || {
1624		let dynamic = ffi::parse_id(dynamic)?;
1625		State::lock().origin.dynamic_close(dynamic)
1626	})
1627}
1628
1629/// The path of a broadcast request delivered to a [moq_origin_dynamic] callback.
1630///
1631/// The destination borrows the request's storage: copy it out before accept,
1632/// reject, or [moq_broadcast_request_free].
1633///
1634/// Returns a zero on success, or a negative code on failure.
1635///
1636/// # Safety
1637/// - `dst` must point at a writable [moq_string].
1638#[unsafe(no_mangle)]
1639pub unsafe extern "C" fn moq_broadcast_request_path(request: u32, dst: *mut moq_string) -> i32 {
1640	ffi::enter(move || {
1641		let request = ffi::parse_id(request)?;
1642		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1643		State::lock().origin.broadcast_request_path(request, dst)
1644	})
1645}
1646
1647/// Accept a broadcast request with an unannounced broadcast producer.
1648///
1649/// Consumes the request handle. Returns a zero on success, or a negative code
1650/// on failure.
1651#[unsafe(no_mangle)]
1652pub extern "C" fn moq_broadcast_request_accept(request: u32, broadcast: u32) -> i32 {
1653	ffi::enter(move || {
1654		let request = ffi::parse_id(request)?;
1655		let broadcast = ffi::parse_id(broadcast)?;
1656		let mut state = State::lock();
1657		let pending = state.origin.broadcast_request_take(request)?;
1658		let consumer = state.publish.producer(broadcast)?.consume();
1659		pending.accept(&consumer);
1660		Ok(())
1661	})
1662}
1663
1664/// Reject a broadcast request with an application error code.
1665///
1666/// Consumes the request handle. Returns a zero on success, or a negative code
1667/// on failure.
1668#[unsafe(no_mangle)]
1669pub extern "C" fn moq_broadcast_request_reject(request: u32, error_code: u16) -> i32 {
1670	ffi::enter(move || {
1671		let request = ffi::parse_id(request)?;
1672		let pending = State::lock().origin.broadcast_request_take(request)?;
1673		pending.reject(moq_net::Error::App(error_code));
1674		Ok(())
1675	})
1676}
1677
1678/// Free a broadcast request without accepting or rejecting it.
1679///
1680/// Dropping the request rejects it. Returns a zero on success, or a negative
1681/// code if the handle is unknown.
1682#[unsafe(no_mangle)]
1683pub extern "C" fn moq_broadcast_request_free(request: u32) -> i32 {
1684	ffi::enter(move || {
1685		let request = ffi::parse_id(request)?;
1686		State::lock().origin.broadcast_request_take(request)?;
1687		Ok(())
1688	})
1689}
1690
1691/// Learn about broadcasts matching a pattern scope under an origin.
1692///
1693/// `prefix` is a literal path root. `filter` is a pattern relative to that
1694/// prefix, or NULL for every path beneath it. Empty is a valid exact filter.
1695/// Delivered [moq_announce_update] prefixes remain relative to the origin.
1696///
1697/// `on_announce` is invoked with a positive announced ID for each broadcast,
1698/// then exactly once more with a terminal code: `0` (stopped cleanly) or a
1699/// negative error. After the terminal (`<= 0`) callback, `on_announce` is never
1700/// called again and `user_data` is never touched again, so release `user_data`
1701/// there. The terminal callback fires even after [moq_origin_announced_cancel].
1702///
1703/// - [moq_origin_announced_info] is used to query information about the broadcast.
1704/// - [moq_origin_announced_free] releases each delivered announced ID once read.
1705/// - [moq_origin_announced_cancel] is used to stop receiving announcements.
1706///
1707/// Returns a non-zero handle on success, or a negative code on failure.
1708///
1709/// # Safety
1710/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_announce` callback.
1711#[unsafe(no_mangle)]
1712pub unsafe extern "C" fn moq_origin_announced(
1713	origin: u32,
1714	prefix: *const c_char,
1715	prefix_len: usize,
1716	filter: *const c_char,
1717	filter_len: usize,
1718	on_announce: ffi::moq_status_callback,
1719	user_data: *mut c_void,
1720) -> i32 {
1721	ffi::enter(move || {
1722		let origin = ffi::parse_id(origin)?;
1723		let prefix = unsafe { ffi::parse_str(prefix, prefix_len)? }.to_string();
1724		let filter = if filter.is_null() {
1725			None
1726		} else {
1727			Some(unsafe { ffi::parse_str(filter, filter_len)? }.to_string())
1728		};
1729		let on_announce = unsafe { ffi::OnStatus::new(user_data, on_announce)? };
1730		State::lock().origin.announced(origin, prefix, filter, on_announce)
1731	})
1732}
1733
1734/// Query information about a broadcast discovered by [moq_origin_announced].
1735///
1736/// The destination is filled with the route information. The `prefix`, `captures`,
1737/// and capture string pointers borrow the announcement's storage: copy them out
1738/// before calling [moq_origin_announced_free], which invalidates them.
1739///
1740/// Returns a zero on success, or a negative code on failure.
1741///
1742/// # Safety
1743/// - The caller must ensure that `dst` is a valid pointer to a [moq_announce_update] struct.
1744#[unsafe(no_mangle)]
1745pub unsafe extern "C" fn moq_origin_announced_info(announced: u32, dst: *mut moq_announce_update) -> i32 {
1746	ffi::enter(move || {
1747		let announced = ffi::parse_id(announced)?;
1748		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1749		State::lock().origin.announced_info(announced, dst)
1750	})
1751}
1752
1753/// Free a single announcement delivered to a [moq_origin_announced] `on_announce` callback.
1754///
1755/// Each announce / unannounce event hands the callback a distinct announcement handle (read
1756/// with [moq_origin_announced_info]); release it here once done to avoid leaking one per event
1757/// over the life of the listener. This is per-announcement and distinct from
1758/// [moq_origin_announced_cancel], which stops the listener itself. After freeing,
1759/// any pointer obtained from [moq_origin_announced_info] for this handle is dangling.
1760///
1761/// Returns zero on success, or a negative code if the handle is unknown.
1762#[unsafe(no_mangle)]
1763pub extern "C" fn moq_origin_announced_free(announced: u32) -> i32 {
1764	ffi::enter(move || {
1765		let announced = ffi::parse_id(announced)?;
1766		State::lock().origin.announced_free(announced)
1767	})
1768}
1769
1770/// Stop receiving announcements for broadcasts published to an origin.
1771///
1772/// Returns immediately: zero on success, or a negative code if already closed.
1773/// Does NOT free `user_data`. The [moq_origin_announced] `on_announce` callback
1774/// still fires once more with a terminal `0` (or a negative error), and that
1775/// final callback is where `user_data` should be released.
1776#[unsafe(no_mangle)]
1777pub extern "C" fn moq_origin_announced_cancel(announced: u32) -> i32 {
1778	ffi::enter(move || {
1779		let announced = ffi::parse_id(announced)?;
1780		State::lock().origin.announced_close(announced)
1781	})
1782}
1783
1784/// Consume a broadcast from an origin by path, waiting until something can serve it.
1785///
1786/// Resolves against future announcements: it waits for the announcement to arrive (e.g. over the
1787/// network) and then delivers the broadcast handle via `on_broadcast`. Use it right after
1788/// [moq_session_connect] to avoid racing announcement gossip. To resolve against only what is
1789/// reachable now, use [moq_origin_request] instead. A broadcast created on this origin
1790/// resolves once it is announced, like a remote one.
1791///
1792/// `on_broadcast` is invoked with a positive broadcast handle once announced, then exactly once
1793/// more with a terminal code: `0` (the wait finished, including after
1794/// [moq_origin_announced_broadcast_cancel]) or a negative error. After the terminal (`<= 0`) callback,
1795/// `on_broadcast` is never called again and `user_data` is never touched again, so release
1796/// `user_data` there. The broadcast handle is usable with [moq_consume_catalog] / [moq_consume_track]
1797/// and must be freed separately with [moq_consume_close].
1798///
1799/// Returns a non-zero handle to the wait on success, or a negative code on (immediate) failure.
1800///
1801/// # Safety
1802/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
1803/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_broadcast` callback.
1804#[unsafe(no_mangle)]
1805pub unsafe extern "C" fn moq_origin_announced_broadcast(
1806	origin: u32,
1807	path: *const c_char,
1808	path_len: usize,
1809	on_broadcast: ffi::moq_status_callback,
1810	user_data: *mut c_void,
1811) -> i32 {
1812	ffi::enter(move || {
1813		let origin = ffi::parse_id(origin)?;
1814		let path = unsafe { ffi::parse_str(path, path_len)? }.to_string();
1815		let on_broadcast = unsafe { ffi::OnStatus::new(user_data, on_broadcast)? };
1816		State::lock().origin.consume_announced(origin, path, on_broadcast)
1817	})
1818}
1819
1820/// Abort a wait started by [moq_origin_announced_broadcast].
1821///
1822/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
1823/// `user_data`. The [moq_origin_announced_broadcast] `on_broadcast` callback still fires once more
1824/// with a terminal `0` (or a negative error), and that final callback is where `user_data` should
1825/// be released. Any broadcast handle already delivered is unaffected and must still be freed with
1826/// [moq_consume_close].
1827#[unsafe(no_mangle)]
1828pub extern "C" fn moq_origin_announced_broadcast_cancel(task: u32) -> i32 {
1829	ffi::enter(move || {
1830		let task = ffi::parse_id(task)?;
1831		State::lock().origin.consume_announced_close(task)
1832	})
1833}
1834
1835/// Request a broadcast from an origin by path, resolving as soon as it can be served.
1836///
1837/// Resolves against what is announced *now*, where [moq_origin_announced_broadcast] waits
1838/// indefinitely: it returns an announced broadcast at once, and fails when none is reachable,
1839/// including a broadcast created but not announced. It does NOT wait for a later
1840/// announcement. Serve on-demand paths with [moq_origin_dynamic].
1841///
1842/// `on_broadcast` is invoked with a positive broadcast handle once served, then exactly once more
1843/// with a terminal code: `0` (finished, including after [moq_origin_request_cancel]) or a negative
1844/// error. After the terminal (`<= 0`) callback, `user_data` is never touched again, so release it
1845/// there. The broadcast handle is usable with [moq_consume_catalog] / [moq_consume_track] and must
1846/// be freed separately with [moq_consume_close].
1847///
1848/// Returns a non-zero handle to the request on success, or a negative code on (immediate) failure.
1849///
1850/// # Safety
1851/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
1852/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_broadcast` callback.
1853#[unsafe(no_mangle)]
1854pub unsafe extern "C" fn moq_origin_request(
1855	origin: u32,
1856	path: *const c_char,
1857	path_len: usize,
1858	on_broadcast: ffi::moq_status_callback,
1859	user_data: *mut c_void,
1860) -> i32 {
1861	ffi::enter(move || {
1862		let origin = ffi::parse_id(origin)?;
1863		let path = unsafe { ffi::parse_str(path, path_len)? }.to_string();
1864		let on_broadcast = unsafe { ffi::OnStatus::new(user_data, on_broadcast)? };
1865		State::lock().origin.request(origin, path, on_broadcast)
1866	})
1867}
1868
1869/// Abort a request started by [moq_origin_request].
1870///
1871/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
1872/// `user_data`; the [moq_origin_request] `on_broadcast` callback fires once more with a terminal
1873/// code, which is where `user_data` should be released. Any broadcast handle already delivered is
1874/// unaffected and must still be freed with [moq_consume_close].
1875#[unsafe(no_mangle)]
1876pub extern "C" fn moq_origin_request_cancel(task: u32) -> i32 {
1877	ffi::enter(move || {
1878		let task = ffi::parse_id(task)?;
1879		State::lock().origin.consume_announced_close(task)
1880	})
1881}
1882
1883/// Close an origin and clean up its resources.
1884///
1885/// The origin keeps running while a broadcast published on it or a
1886/// [moq_origin_dynamic] handler lives; close or cancel those to end it.
1887///
1888/// Returns a zero on success, or a negative code on failure.
1889#[unsafe(no_mangle)]
1890pub extern "C" fn moq_origin_close(origin: u32) -> i32 {
1891	ffi::enter(move || {
1892		let origin = ffi::parse_id(origin)?;
1893		State::lock().origin.close(origin)
1894	})
1895}
1896
1897/// Advertise a broadcast's exact path as a route.
1898///
1899/// Announcing again re-prices the route in place. A NULL `route` uses the default
1900/// (no hops, cost 0). Until announced, the broadcast is invisible and unroutable for
1901/// local consumers and peers alike.
1902///
1903/// Returns a zero on success, or a negative code on failure.
1904///
1905/// # Safety
1906/// - `route` may be NULL, or must point at a readable [moq_route].
1907#[unsafe(no_mangle)]
1908pub unsafe extern "C" fn moq_publish_announce(broadcast: u32, route: *const moq_route) -> i32 {
1909	ffi::enter(move || {
1910		let broadcast = ffi::parse_id(broadcast)?;
1911		let route = unsafe { parse_route(route)? };
1912		State::lock().publish.announce(broadcast, route)
1913	})
1914}
1915
1916/// Retract a broadcast's exact-path advertisement, if any.
1917///
1918/// Local consumers and peers alike stop discovering and requesting it; tracks already in
1919/// flight carry on, and announcing again brings it back. Returns a zero on success, or a
1920/// negative code on failure.
1921#[unsafe(no_mangle)]
1922pub extern "C" fn moq_publish_unannounce(broadcast: u32) -> i32 {
1923	ffi::enter(move || {
1924		let broadcast = ffi::parse_id(broadcast)?;
1925		State::lock().publish.unannounce(broadcast)
1926	})
1927}
1928
1929/// End a broadcast for good and release its handle.
1930///
1931/// The origin retracts the path immediately and serves no new tracks; tracks already
1932/// subscribed carry on to their own end. The handle is invalid afterwards, so closing
1933/// it again fails like any unknown handle.
1934///
1935/// Returns a zero on success, or a negative code on failure.
1936#[unsafe(no_mangle)]
1937pub extern "C" fn moq_publish_close(broadcast: u32) -> i32 {
1938	ffi::enter(move || {
1939		let broadcast = ffi::parse_id(broadcast)?;
1940		State::lock().publish.close(broadcast)
1941	})
1942}
1943
1944/// Deprecated: use [moq_publish_close]. A broadcast end carries no cause.
1945///
1946/// Returns a zero on success, or a negative code on failure.
1947#[unsafe(no_mangle)]
1948pub extern "C" fn moq_publish_finish(broadcast: u32) -> i32 {
1949	moq_publish_close(broadcast)
1950}
1951
1952/// Publish one audio codec as a new media track.
1953///
1954/// The track is named after the format (`0.opus`), so a subscriber finds it
1955/// through the catalog rather than by a name you choose.
1956/// [moq_audio_init::init] is required: audio resolves its whole rendition from
1957/// those bytes. Frames written with [moq_publish_media_frame] must be in decode
1958/// order.
1959///
1960/// Returns a non-zero handle to the track on success, or a negative code on failure.
1961///
1962/// # Safety
1963/// - `config` must be NULL, or point to an aligned, readable [moq_audio_init].
1964///   Every non-NULL pointer inside it must be valid for its paired length and
1965///   stay alive for the duration of this call. A NULL config is rejected with an
1966///   ordinary error.
1967#[unsafe(no_mangle)]
1968pub unsafe extern "C" fn moq_publish_audio(broadcast: u32, config: *const moq_audio_init) -> i32 {
1969	ffi::enter(move || {
1970		let broadcast = ffi::parse_id(broadcast)?;
1971		let audio = unsafe { parse_audio_init(config)? };
1972		State::lock().publish.audio(broadcast, audio)
1973	})
1974}
1975
1976/// # Safety
1977/// - As [moq_publish_audio], for `config`.
1978unsafe fn parse_audio_init(config: *const moq_audio_init) -> Result<moq_mux::import::AudioInit, Error> {
1979	let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1980	let init = unsafe { ffi::parse_slice(config.init, config.init_len)? };
1981	let label = unsafe { ffi::parse_str_optional(config.label, config.label_len)? };
1982
1983	let mut audio = moq_mux::import::AudioInit::new(audio_format_from_u32(config.format)?, init.to_vec());
1984	audio.label = label.map(str::to_string);
1985	Ok(audio)
1986}
1987
1988/// Publish one video codec as a new media track.
1989///
1990/// Named as in [moq_publish_audio]. [moq_video_init::init] may be NULL for a
1991/// format that resolves in band.
1992///
1993/// Returns a non-zero handle to the track on success, or a negative code on failure.
1994///
1995/// # Safety
1996/// - As [moq_publish_audio], for a [moq_video_init].
1997#[unsafe(no_mangle)]
1998pub unsafe extern "C" fn moq_publish_video(broadcast: u32, config: *const moq_video_init) -> i32 {
1999	ffi::enter(move || {
2000		let broadcast = ffi::parse_id(broadcast)?;
2001		let video = unsafe { parse_video_init(config)? };
2002		State::lock().publish.video(broadcast, video)
2003	})
2004}
2005
2006/// # Safety
2007/// - As [moq_publish_audio], for a [moq_video_init].
2008unsafe fn parse_video_init(config: *const moq_video_init) -> Result<moq_mux::import::VideoInit, Error> {
2009	let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
2010	let init = unsafe { ffi::parse_slice(config.init, config.init_len)? };
2011	let label = unsafe { ffi::parse_str_optional(config.label, config.label_len)? };
2012
2013	let mut video = moq_mux::import::VideoInit::new(video_format_from_u32(config.format)?, init.to_vec());
2014	video.label = label.map(str::to_string);
2015	video.hint = config.hint.resolve();
2016	Ok(video)
2017}
2018
2019/// Publish a container, which demuxes and publishes its own tracks.
2020///
2021/// Feed it whole chunks with [moq_publish_container_write]. Unlike the codec
2022/// entry points there is no label: a container describes each track it publishes
2023/// from its own metadata.
2024///
2025/// Returns a non-zero handle to the container on success, or a negative code on failure.
2026///
2027/// # Safety
2028/// - As [moq_publish_audio], for a [moq_container_init].
2029#[unsafe(no_mangle)]
2030pub unsafe extern "C" fn moq_publish_container(broadcast: u32, config: *const moq_container_init) -> i32 {
2031	ffi::enter(move || {
2032		let broadcast = ffi::parse_id(broadcast)?;
2033		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
2034		let init = unsafe { ffi::parse_slice(config.init, config.init_len)? };
2035
2036		let container = moq_mux::import::ContainerInit::new(container_format_from_u32(config.format)?, init.to_vec());
2037		State::lock().publish.container(broadcast, container)
2038	})
2039}
2040
2041/// Draw a group boundary on a media importer.
2042///
2043/// For a codec track this ends the open group; the next frame written starts a new one. Audio has
2044/// no boundary of its own (every packet is independently decodable), so this is the only thing
2045/// that gives it groups: call it after every frame for one group (one QUIC stream) the relay
2046/// forwards without waiting, or at a segment cadence to align with video for HLS/DASH. Video
2047/// groups at its own keyframes and needs this only to override that.
2048///
2049/// A container has its own [moq_publish_container_cut], since it rolls a group on every track it
2050/// publishes rather than ending one group.
2051///
2052/// Returns a zero on success, or a negative code on failure.
2053#[unsafe(no_mangle)]
2054pub extern "C" fn moq_publish_media_cut(media: u32) -> i32 {
2055	ffi::enter(move || {
2056		let media = ffi::parse_id(media)?;
2057		State::lock().publish.media_cut(media)
2058	})
2059}
2060
2061/// Draw a group boundary and number the next group `sequence`.
2062///
2063/// [moq_publish_media_cut] with an explicit sequence, for a caller whose group numbers have to be
2064/// deterministic: two encoders publishing the same content align per GOP so a consumer can fail
2065/// over between them.
2066///
2067/// Returns a zero on success, or a negative code on failure.
2068#[unsafe(no_mangle)]
2069pub extern "C" fn moq_publish_media_seek(media: u32, sequence: u64) -> i32 {
2070	ffi::enter(move || {
2071		let media = ffi::parse_id(media)?;
2072		State::lock().publish.media_seek(media, sequence)
2073	})
2074}
2075
2076/// Finish a media track, flushing any buffered frames. No more frames can be written.
2077///
2078/// Returns a zero on success, or a negative code on failure.
2079#[unsafe(no_mangle)]
2080pub extern "C" fn moq_publish_media_finish(export: u32) -> i32 {
2081	ffi::enter(move || {
2082		let export = ffi::parse_id(export)?;
2083		State::lock().publish.media_finish(export)
2084	})
2085}
2086
2087/// Watch whether a media track has subscribers, so an encoder runs only while someone watches.
2088///
2089/// `on_demand` fires right away with the current [moq_demand] state, again on every
2090/// change, then exactly once more with a terminal code: `0` (the track ended or the
2091/// watcher was stopped with [moq_publish_demand_cancel]) or a negative error. After the
2092/// terminal (`<= 0`) callback, `user_data` is never touched again. Reporting the current
2093/// state first means a track that went unused before the watcher existed still reports it.
2094///
2095/// A container handle is refused: it publishes several tracks and has no single demand.
2096///
2097/// Returns a non-zero watcher handle on success, or a negative code on failure.
2098///
2099/// # Safety
2100/// - `on_demand` must be non-NULL.
2101/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_demand` callback.
2102#[unsafe(no_mangle)]
2103pub unsafe extern "C" fn moq_publish_media_demand(
2104	media: u32,
2105	on_demand: ffi::moq_status_callback,
2106	user_data: *mut c_void,
2107) -> i32 {
2108	ffi::enter(move || {
2109		let media = ffi::parse_id(media)?;
2110		let on_demand = unsafe { ffi::OnStatus::new(user_data, on_demand)? };
2111		let mut state = State::lock();
2112		let demand = state.publish.media_demand(media)?;
2113		state.publish.demand(demand, on_demand)
2114	})
2115}
2116
2117/// Stop a demand watcher from [moq_publish_track_demand], [moq_publish_media_demand],
2118/// [`crate::moq_encode_video_demand`], or [`crate::moq_encode_audio_demand`].
2119///
2120/// Returns immediately: zero on success, or a negative code if already closed. The
2121/// watcher's `on_demand` callback still fires once more with a terminal `0`, and
2122/// that final callback is where `user_data` should be released.
2123#[unsafe(no_mangle)]
2124pub extern "C" fn moq_publish_demand_cancel(watcher: u32) -> i32 {
2125	ffi::enter(move || {
2126		let watcher = ffi::parse_id(watcher)?;
2127		State::lock().publish.demand_close(watcher)
2128	})
2129}
2130
2131/// Write a whole chunk of container bytes.
2132///
2133/// No timestamp: a container carries its tracks' timing itself, and the importer
2134/// reads it out rather than taking the caller's word for it.
2135///
2136/// Returns zero on success, or a negative code on failure.
2137///
2138/// # Safety
2139/// - The caller must ensure `payload` is valid for `payload_size` bytes.
2140#[unsafe(no_mangle)]
2141pub unsafe extern "C" fn moq_publish_container_write(container: u32, payload: *const u8, payload_size: usize) -> i32 {
2142	ffi::enter(move || {
2143		let container = ffi::parse_id(container)?;
2144		let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
2145		State::lock().publish.container_write(container, payload)
2146	})
2147}
2148
2149/// Declare that the next chunk starts a new segment, rolling a group on every
2150/// track the container publishes.
2151///
2152/// An fMP4 source carrying `styp` atoms declares its own segments, so this is
2153/// only needed when it doesn't. Formats with no segment concept (MKV, TS, FLV)
2154/// ignore it.
2155///
2156/// Returns zero on success, or a negative code on failure.
2157#[unsafe(no_mangle)]
2158pub extern "C" fn moq_publish_container_cut(container: u32) -> i32 {
2159	ffi::enter(move || {
2160		let container = ffi::parse_id(container)?;
2161		State::lock().publish.container_cut(container)
2162	})
2163}
2164
2165/// Start a new segment and number its groups `sequence`.
2166///
2167/// Returns zero on success, or a negative code on failure.
2168#[unsafe(no_mangle)]
2169pub extern "C" fn moq_publish_container_seek(container: u32, sequence: u64) -> i32 {
2170	ffi::enter(move || {
2171		let container = ffi::parse_id(container)?;
2172		State::lock().publish.container_seek(container, sequence)
2173	})
2174}
2175
2176/// Finish every track the container publishes and release the handle.
2177///
2178/// Returns zero on success, or a negative code on failure.
2179#[unsafe(no_mangle)]
2180pub extern "C" fn moq_publish_container_finish(container: u32) -> i32 {
2181	ffi::enter(move || {
2182		let container = ffi::parse_id(container)?;
2183		State::lock().publish.container_finish(container)
2184	})
2185}
2186
2187/// Write data to a track.
2188///
2189/// The encoding of `data` depends on the track `format`.
2190/// The timestamp is in microseconds.
2191///
2192/// Returns a zero on success, or a negative code on failure.
2193///
2194/// # Safety
2195/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
2196#[unsafe(no_mangle)]
2197pub unsafe extern "C" fn moq_publish_media_frame(
2198	media: u32,
2199	payload: *const u8,
2200	payload_size: usize,
2201	timestamp_us: u64,
2202) -> i32 {
2203	ffi::enter(move || {
2204		let media = ffi::parse_id(media)?;
2205		let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
2206		let timestamp = hang::container::Timestamp::from_micros(timestamp_us)?;
2207		State::lock().publish.media_frame(media, payload, timestamp)
2208	})
2209}
2210
2211/// Record the transport handoff of one locally encoded frame for catalog jitter.
2212///
2213/// `timestamp_us` is the frame's presentation time on the broadcast media clock. Call this
2214/// after [moq_publish_media_frame] only for local encoder output; file, pipe, and network imports
2215/// must remain clock-free. The monotonic handoff time is sampled inside this process.
2216///
2217/// Returns zero on success, or a negative code for an invalid handle or timestamp.
2218#[unsafe(no_mangle)]
2219pub extern "C" fn moq_publish_media_flush(media: u32, timestamp_us: u64) -> i32 {
2220	ffi::enter(move || {
2221		let media = ffi::parse_id(media)?;
2222		let timestamp = hang::container::Timestamp::from_micros(timestamp_us)?;
2223		State::lock().publish.media_flush(media, timestamp)
2224	})
2225}
2226
2227/// Mark a timeline break and restart handoff measurement without lowering advertised jitter.
2228///
2229/// Publishes a discontinuity marker; resumed frames must continue the broadcast media clock,
2230/// and video must resume on a keyframe.
2231/// Returns zero on success, or a negative code on failure.
2232#[unsafe(no_mangle)]
2233pub extern "C" fn moq_publish_media_discontinuity(media: u32) -> i32 {
2234	ffi::enter(move || {
2235		let media = ffi::parse_id(media)?;
2236		State::lock().publish.media_discontinuity(media)
2237	})
2238}
2239
2240/// Replace the catalog properties shared by every video rendition.
2241///
2242/// 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.
2243///
2244/// Returns zero on success, or a negative code on failure.
2245///
2246/// # Safety
2247/// - The caller must ensure that `properties` points to a valid [moq_video_properties].
2248#[unsafe(no_mangle)]
2249pub unsafe extern "C" fn moq_publish_video_properties(broadcast: u32, properties: *const moq_video_properties) -> i32 {
2250	ffi::enter(move || {
2251		let broadcast = ffi::parse_id(broadcast)?;
2252		let properties = unsafe { properties.as_ref() }.ok_or(Error::InvalidPointer)?;
2253
2254		let mut value = hang::catalog::VideoProperties::default();
2255		value.display = properties.has_display.then_some(hang::catalog::Display {
2256			width: properties.display_width,
2257			height: properties.display_height,
2258		});
2259		value.rotation = properties.has_rotation.then_some(properties.rotation);
2260		value.flip = properties.has_flip.then_some(properties.flip);
2261
2262		State::lock().publish.video_properties(broadcast, value)
2263	})
2264}
2265
2266/// Add or replace a video rendition in a broadcast's catalog.
2267///
2268/// This is the producer counterpart to [moq_consume_video_config]: instead of
2269/// reading a rendition out of a catalog, it writes one into the catalog of a
2270/// broadcast created with [moq_origin_create_broadcast]. The rendition is keyed by
2271/// `config.name`; calling this again with the same name replaces the rendition
2272/// you declared, so a config can be refined in place. It fails only when a
2273/// [moq_publish_video] track owns the name, since that track publishes and
2274/// retires its own rendition. The updated catalog is published to subscribers
2275/// automatically.
2276///
2277/// The struct fields are read as inputs:
2278/// - `name` / `codec` are required (NOT NULL terminated) string slices.
2279/// - `label` may be NULL to omit the human-readable rendition name.
2280/// - `description` may be NULL to omit it.
2281/// - `coded_width` / `coded_height` may be zero to omit them.
2282/// - `container` describes how the frames written to the track are wrapped. A
2283///   zeroed one declares the legacy container, which is what [moq_publish_video]
2284///   writes; declare CMAF or LOC for a [moq_publish_track] whose frames you
2285///   already encode that way.
2286///
2287/// Returns a zero on success, or a negative code on failure.
2288///
2289/// # Safety
2290/// - The caller must ensure that `config` points to a valid [moq_video_config].
2291/// - The caller must ensure each non-NULL pointer inside `config` is valid for its length.
2292#[unsafe(no_mangle)]
2293pub unsafe extern "C" fn moq_publish_video_config(broadcast: u32, config: *const moq_video_config) -> i32 {
2294	ffi::enter(move || {
2295		let broadcast = ffi::parse_id(broadcast)?;
2296		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
2297
2298		let name = unsafe { ffi::parse_str(config.name, config.name_len)? };
2299		let label = unsafe { ffi::parse_str_optional(config.label, config.label_len)? };
2300		let codec = unsafe { ffi::parse_str(config.codec, config.codec_len)? };
2301		let codec = hang::catalog::VideoCodec::from_str(codec).map_err(Error::Hang)?;
2302
2303		let mut video = hang::catalog::VideoConfig::new(codec);
2304		video.label = label.map(str::to_string);
2305		if !config.description.is_null() {
2306			let description = unsafe { ffi::parse_slice(config.description, config.description_len)? };
2307			video.description = Some(bytes::Bytes::copy_from_slice(description));
2308		}
2309		video.coded_width = (config.coded_width > 0).then_some(config.coded_width);
2310		video.coded_height = (config.coded_height > 0).then_some(config.coded_height);
2311		video.container = unsafe { parse_container(&config.container)? };
2312
2313		State::lock().publish.video_config(broadcast, name, video)
2314	})
2315}
2316
2317/// Add or replace an audio rendition in a broadcast's catalog.
2318///
2319/// This is the producer counterpart to [moq_consume_audio_config]. The rendition
2320/// is keyed by `config.name`, on the same terms as [moq_publish_video_config]:
2321/// a repeat call replaces your own rendition, and a name a [moq_publish_audio]
2322/// track owns is refused. The updated catalog is published to subscribers
2323/// automatically.
2324///
2325/// The struct fields are read as inputs:
2326/// - `name` / `codec` are required (NOT NULL terminated) string slices.
2327/// - `label` may be NULL to omit the human-readable rendition name.
2328/// - `sample_rate` / `channel_count` are required.
2329/// - `description` may be NULL to omit it.
2330/// - `container` describes how the frames written to the track are wrapped, the
2331///   same as for [moq_publish_video_config].
2332///
2333/// Returns a zero on success, or a negative code on failure.
2334///
2335/// # Safety
2336/// - The caller must ensure that `config` points to a valid [moq_audio_config].
2337/// - The caller must ensure each non-NULL pointer inside `config` is valid for its length.
2338#[unsafe(no_mangle)]
2339pub unsafe extern "C" fn moq_publish_audio_config(broadcast: u32, config: *const moq_audio_config) -> i32 {
2340	ffi::enter(move || {
2341		let broadcast = ffi::parse_id(broadcast)?;
2342		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
2343
2344		let name = unsafe { ffi::parse_str(config.name, config.name_len)? };
2345		let label = unsafe { ffi::parse_str_optional(config.label, config.label_len)? };
2346		let codec = unsafe { ffi::parse_str(config.codec, config.codec_len)? };
2347		let codec = hang::catalog::AudioCodec::from_str(codec).map_err(Error::Hang)?;
2348
2349		let mut audio = hang::catalog::AudioConfig::new(codec, config.sample_rate, config.channel_count);
2350		audio.label = label.map(str::to_string);
2351		audio.container = unsafe { parse_container(&config.container)? };
2352		if !config.description.is_null() {
2353			let description = unsafe { ffi::parse_slice(config.description, config.description_len)? };
2354			audio.description = Some(bytes::Bytes::copy_from_slice(description));
2355		}
2356
2357		State::lock().publish.audio_config(broadcast, name, audio)
2358	})
2359}
2360
2361/// Remove a video rendition from a broadcast's catalog by name.
2362///
2363/// Removes a rendition added by [moq_publish_video_config]. Any other name is a
2364/// no-op, including one a [moq_publish_video] track owns, which is retired by
2365/// [moq_publish_media_finish] instead. The updated catalog is published to
2366/// subscribers automatically.
2367///
2368/// Returns a zero on success, or a negative code on failure.
2369///
2370/// # Safety
2371/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
2372#[unsafe(no_mangle)]
2373pub unsafe extern "C" fn moq_publish_video_remove(broadcast: u32, name: *const c_char, name_len: usize) -> i32 {
2374	ffi::enter(move || {
2375		let broadcast = ffi::parse_id(broadcast)?;
2376		let name = unsafe { ffi::parse_str(name, name_len)? };
2377		State::lock().publish.video_remove(broadcast, name)
2378	})
2379}
2380
2381/// Remove an audio rendition from a broadcast's catalog by name.
2382///
2383/// Same rules as [moq_publish_video_remove].
2384///
2385/// Returns a zero on success, or a negative code on failure.
2386///
2387/// # Safety
2388/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
2389#[unsafe(no_mangle)]
2390pub unsafe extern "C" fn moq_publish_audio_remove(broadcast: u32, name: *const c_char, name_len: usize) -> i32 {
2391	ffi::enter(move || {
2392		let broadcast = ffi::parse_id(broadcast)?;
2393		let name = unsafe { ffi::parse_str(name, name_len)? };
2394		State::lock().publish.audio_remove(broadcast, name)
2395	})
2396}
2397
2398/// Set (or replace) a top-level application catalog section by name.
2399///
2400/// This is the producer counterpart to [moq_consume_catalog_section] /
2401/// [moq_consume_catalog_section_at]: it writes an arbitrary top-level JSON key into the
2402/// catalog of a broadcast created with [moq_origin_create_broadcast], beyond the
2403/// `video`/`audio` keys owned by the media pipeline. Calling it again with the
2404/// same name replaces the section. The updated catalog is published to
2405/// subscribers automatically.
2406///
2407/// `json` is a JSON document (object, array, string, ...) as `json_len` bytes of
2408/// UTF-8. Returns a zero on success, or a negative code on failure: invalid JSON
2409/// yields a Json error (-37); a reserved `name` (`video`/`audio`) yields a mux error.
2410///
2411/// # Safety
2412/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
2413/// - The caller must ensure that json is a valid pointer to json_len bytes of data.
2414#[unsafe(no_mangle)]
2415pub unsafe extern "C" fn moq_publish_catalog_section(
2416	broadcast: u32,
2417	name: *const c_char,
2418	name_len: usize,
2419	json: *const c_char,
2420	json_len: usize,
2421) -> i32 {
2422	ffi::enter(move || {
2423		let broadcast = ffi::parse_id(broadcast)?;
2424		let name = unsafe { ffi::parse_str(name, name_len)? };
2425		let json = unsafe { ffi::parse_str(json, json_len)? };
2426		let value: serde_json::Value = serde_json::from_str(json)?;
2427		State::lock().publish.catalog_section_set(broadcast, name, value)
2428	})
2429}
2430
2431/// Remove a top-level application catalog section by name.
2432///
2433/// This is a no-op if no section with that name exists. The updated catalog is
2434/// published to subscribers automatically.
2435///
2436/// Returns a zero on success, or a negative code on failure.
2437///
2438/// # Safety
2439/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
2440#[unsafe(no_mangle)]
2441pub unsafe extern "C" fn moq_publish_catalog_section_remove(
2442	broadcast: u32,
2443	name: *const c_char,
2444	name_len: usize,
2445) -> i32 {
2446	ffi::enter(move || {
2447		let broadcast = ffi::parse_id(broadcast)?;
2448		let name = unsafe { ffi::parse_str(name, name_len)? };
2449		State::lock().publish.catalog_section_remove(broadcast, name)
2450	})
2451}
2452
2453/// Create a raw track on a broadcast for arbitrary byte payloads.
2454///
2455/// Unlike [moq_publish_audio] and [moq_publish_video], this is the bare moq-net primitive: no
2456/// codec, container, or catalog framing. Frames written to it are delivered
2457/// as-is to subscribers using [moq_consume_track]. Use it for non-media tracks
2458/// (control channels, JSON metadata, etc.), or pair it with
2459/// [moq_publish_video_config] / [moq_publish_audio_config] to also describe the
2460/// track in the catalog. Pass NULL for `info` to use moq-net defaults.
2461///
2462/// Returns a non-zero handle to the track on success, or a negative code on failure.
2463///
2464/// # Safety
2465/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
2466/// - The caller must ensure that info is either NULL or a valid pointer to a [moq_track_info] struct.
2467#[unsafe(no_mangle)]
2468pub unsafe extern "C" fn moq_publish_track(
2469	broadcast: u32,
2470	name: *const c_char,
2471	name_len: usize,
2472	info: *const moq_track_info,
2473) -> i32 {
2474	ffi::enter(move || {
2475		let broadcast = ffi::parse_id(broadcast)?;
2476		let name = unsafe { ffi::parse_str(name, name_len)? };
2477		let info = unsafe { parse_track_info(info)? };
2478		State::lock().publish.track(broadcast, name, Some(info))
2479	})
2480}
2481
2482/// Raw track info from an optional C struct, defaulting to a microsecond timescale.
2483///
2484/// # Safety
2485/// - `info` must be NULL or a valid pointer to a [moq_track_info] struct.
2486unsafe fn parse_track_info(info: *const moq_track_info) -> Result<moq_net::track::Info, Error> {
2487	// Default raw tracks to a microsecond timescale even when no info is given.
2488	match unsafe { info.as_ref() } {
2489		Some(info) => moq_net::track::Info::try_from(info),
2490		None => Ok(moq_net::track::Info::default().with_timescale(moq_net::Timescale::MICRO)),
2491	}
2492}
2493
2494/// Append a new group to a raw track, returning a group producer.
2495///
2496/// Groups are delivered independently and each may contain any number of frames
2497/// written via [moq_publish_group_frame]. Sequence numbers auto-increment.
2498///
2499/// Returns a non-zero handle to the group on success, or a negative code on failure.
2500#[unsafe(no_mangle)]
2501pub extern "C" fn moq_publish_track_group(track: u32) -> i32 {
2502	ffi::enter(move || {
2503		let track = ffi::parse_id(track)?;
2504		State::lock().publish.track_group(track)
2505	})
2506}
2507
2508/// Create a raw group with an explicit sequence number.
2509///
2510/// Returns a non-zero group handle on success, or a negative code on failure.
2511#[unsafe(no_mangle)]
2512pub extern "C" fn moq_publish_track_group_at(track: u32, sequence: u64) -> i32 {
2513	ffi::enter(move || {
2514		let track = ffi::parse_id(track)?;
2515		State::lock().publish.track_group_at(track, sequence)
2516	})
2517}
2518
2519/// Write a single-frame group to a raw track with a timestamp.
2520///
2521/// Convenience for the common one-frame-per-group pattern. Equivalent to
2522/// appending a group, writing one frame, and finishing it.
2523/// The timestamp is in microseconds.
2524///
2525/// Returns a zero on success, or a negative code on failure.
2526///
2527/// # Safety
2528/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
2529#[unsafe(no_mangle)]
2530pub unsafe extern "C" fn moq_publish_track_frame(
2531	track: u32,
2532	payload: *const u8,
2533	payload_size: usize,
2534	timestamp_us: u64,
2535) -> i32 {
2536	ffi::enter(move || {
2537		let track = ffi::parse_id(track)?;
2538		let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
2539		let timestamp = moq_net::Timestamp::from_micros(timestamp_us)?;
2540		State::lock().publish.track_frame(track, timestamp, payload)
2541	})
2542}
2543
2544/// Send a best-effort datagram on a raw track created by [moq_publish_track].
2545///
2546/// Takes `payload` then `timestamp_us`, matching [moq_publish_track_frame]. The payload must
2547/// be at most 1200 bytes. On success the datagram's per-track sequence number (shared with the
2548/// group namespace) is written to `out_sequence` when it is non-NULL. Datagrams are
2549/// delivered only on transports and wire versions with a datagram channel; there is no
2550/// group fallback.
2551///
2552/// Returns a zero on success, or a negative code on failure.
2553///
2554/// # Safety
2555/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
2556/// - `out_sequence` must be NULL or a valid pointer to a `uint64_t`.
2557#[unsafe(no_mangle)]
2558pub unsafe extern "C" fn moq_publish_track_datagram(
2559	track: u32,
2560	payload: *const u8,
2561	payload_size: usize,
2562	timestamp_us: u64,
2563	out_sequence: *mut u64,
2564) -> i32 {
2565	ffi::enter(move || {
2566		let track = ffi::parse_id(track)?;
2567		let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
2568		let sequence = State::lock().publish.track_datagram(track, timestamp_us, payload)?;
2569		if let Some(out) = unsafe { out_sequence.as_mut() } {
2570			*out = sequence;
2571		}
2572		Ok(())
2573	})
2574}
2575
2576/// Finish a raw track. No more groups or frames can be written.
2577///
2578/// Returns a zero on success, or a negative code on failure.
2579#[unsafe(no_mangle)]
2580pub extern "C" fn moq_publish_track_finish(track: u32) -> i32 {
2581	ffi::enter(move || {
2582		let track = ffi::parse_id(track)?;
2583		State::lock().publish.track_finish(track)
2584	})
2585}
2586
2587/// Declare a raw track's exclusive final group sequence.
2588///
2589/// Groups below `final_sequence` may still be created. Groups at or above it
2590/// are rejected. The track remains open for groups below the boundary. Call
2591/// [moq_publish_track_finish] after producing the remaining groups.
2592#[unsafe(no_mangle)]
2593pub extern "C" fn moq_publish_track_finish_at(track: u32, final_sequence: u64) -> i32 {
2594	ffi::enter(move || {
2595		let track = ffi::parse_id(track)?;
2596		State::lock().publish.track_finish_at(track, final_sequence)
2597	})
2598}
2599
2600/// Abort a raw track with an application error code.
2601#[unsafe(no_mangle)]
2602pub extern "C" fn moq_publish_track_abort(track: u32, error_code: u16) -> i32 {
2603	ffi::enter(move || {
2604		let track = ffi::parse_id(track)?;
2605		State::lock().publish.track_abort(track, error_code)
2606	})
2607}
2608
2609/// Watch whether a raw track has subscribers. See [moq_publish_media_demand] for the
2610/// callback contract.
2611///
2612/// Returns a non-zero watcher handle on success, or a negative code on failure.
2613///
2614/// # Safety
2615/// - `on_demand` must be non-NULL.
2616/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_demand` callback.
2617#[unsafe(no_mangle)]
2618pub unsafe extern "C" fn moq_publish_track_demand(
2619	track: u32,
2620	on_demand: ffi::moq_status_callback,
2621	user_data: *mut c_void,
2622) -> i32 {
2623	ffi::enter(move || {
2624		let track = ffi::parse_id(track)?;
2625		let on_demand = unsafe { ffi::OnStatus::new(user_data, on_demand)? };
2626		let mut state = State::lock();
2627		let demand = state.publish.track_demand(track)?;
2628		state.publish.demand(demand, on_demand)
2629	})
2630}
2631
2632/// Serve subscriber requests for tracks the broadcast has not declared.
2633///
2634/// Without a live handler a subscription to an unknown track name is refused. While one
2635/// is live, `on_request` is invoked with a positive request handle for each pending
2636/// track, then exactly once more with a terminal code: `0` (the broadcast finished, or
2637/// [moq_publish_dynamic_cancel] was called) or a negative error. After the terminal
2638/// (`<= 0`) callback, `user_data` is never touched again. Answer each request with
2639/// [moq_track_request_accept], [moq_track_request_video], [moq_track_request_audio],
2640/// or [moq_track_request_abort]; the subscriber waits until you do.
2641///
2642/// Returns a non-zero handle on success, or a negative code on failure.
2643///
2644/// # Safety
2645/// - `on_request` must be non-NULL.
2646/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_request` callback.
2647#[unsafe(no_mangle)]
2648pub unsafe extern "C" fn moq_publish_dynamic(
2649	broadcast: u32,
2650	on_request: ffi::moq_status_callback,
2651	user_data: *mut c_void,
2652) -> i32 {
2653	ffi::enter(move || {
2654		let broadcast = ffi::parse_id(broadcast)?;
2655		let on_request = unsafe { ffi::OnStatus::new(user_data, on_request)? };
2656		State::lock().publish.dynamic(broadcast, on_request)
2657	})
2658}
2659
2660/// Serve fetches of groups a raw track no longer has cached.
2661///
2662/// Without a live handler a fetch that misses the cache fails as not found. While one is
2663/// live, `on_group` is invoked with a positive group-request handle for each miss, then
2664/// exactly once more with a terminal code: `0` (the track ended, or
2665/// [moq_publish_dynamic_cancel] was called) or a negative error. After the terminal
2666/// (`<= 0`) callback, `user_data` is never touched again. Cached groups never reach the
2667/// handler. Answer each request with [moq_group_request_accept] or [moq_group_request_abort].
2668///
2669/// Returns a non-zero handle on success, or a negative code on failure.
2670///
2671/// # Safety
2672/// - `on_group` must be non-NULL.
2673/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_group` callback.
2674#[unsafe(no_mangle)]
2675pub unsafe extern "C" fn moq_publish_track_dynamic(
2676	track: u32,
2677	on_group: ffi::moq_status_callback,
2678	user_data: *mut c_void,
2679) -> i32 {
2680	ffi::enter(move || {
2681		let track = ffi::parse_id(track)?;
2682		let on_group = unsafe { ffi::OnStatus::new(user_data, on_group)? };
2683		State::lock().publish.track_dynamic(track, on_group)
2684	})
2685}
2686
2687/// Stop a request handler from [moq_publish_dynamic], [moq_publish_track_dynamic], or
2688/// [moq_track_request_dynamic]. Requests not yet delivered are rejected.
2689///
2690/// Returns immediately: zero on success, or a negative code if already closed. The
2691/// handler's callback still fires once more with a terminal `0`, and that final
2692/// callback is where `user_data` should be released.
2693#[unsafe(no_mangle)]
2694pub extern "C" fn moq_publish_dynamic_cancel(dynamic: u32) -> i32 {
2695	ffi::enter(move || {
2696		let dynamic = ffi::parse_id(dynamic)?;
2697		State::lock().publish.dynamic_close(dynamic)
2698	})
2699}
2700
2701/// The name of a track request delivered to a [moq_publish_dynamic] callback.
2702///
2703/// The destination borrows the request's storage: copy it out before accepting,
2704/// aborting, or freeing the request.
2705///
2706/// Returns a zero on success, or a negative code on failure.
2707///
2708/// # Safety
2709/// - `dst` must point at a writable [moq_string].
2710#[unsafe(no_mangle)]
2711pub unsafe extern "C" fn moq_track_request_name(request: u32, dst: *mut moq_string) -> i32 {
2712	ffi::enter(move || {
2713		let request = ffi::parse_id(request)?;
2714		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2715		State::lock().publish.track_request_name(request, dst)
2716	})
2717}
2718
2719/// Serve fetches of uncached groups on a requested track, before accepting it.
2720///
2721/// A track requested by a fetch has that group pending from birth. Register the
2722/// handler here, before [moq_track_request_accept], so the request survives the
2723/// transition; the callback contract is that of [moq_publish_track_dynamic].
2724///
2725/// Returns a non-zero handle on success, or a negative code on failure.
2726///
2727/// # Safety
2728/// - `on_group` must be non-NULL.
2729/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_group` callback.
2730#[unsafe(no_mangle)]
2731pub unsafe extern "C" fn moq_track_request_dynamic(
2732	request: u32,
2733	on_group: ffi::moq_status_callback,
2734	user_data: *mut c_void,
2735) -> i32 {
2736	ffi::enter(move || {
2737		let request = ffi::parse_id(request)?;
2738		let on_group = unsafe { ffi::OnStatus::new(user_data, on_group)? };
2739		State::lock().publish.track_request_dynamic(request, on_group)
2740	})
2741}
2742
2743/// Accept a track request as a raw track, resolving the waiting subscribers.
2744///
2745/// Consumes the request handle. `info` is as in [moq_publish_track]: NULL for the
2746/// microsecond default. Returns a non-zero track handle usable with every
2747/// `moq_publish_track_*` function, or a negative code on failure.
2748///
2749/// # Safety
2750/// - `info` must be NULL or a valid pointer to a [moq_track_info] struct.
2751#[unsafe(no_mangle)]
2752pub unsafe extern "C" fn moq_track_request_accept(request: u32, info: *const moq_track_info) -> i32 {
2753	ffi::enter(move || {
2754		let request = ffi::parse_id(request)?;
2755		let info = unsafe { parse_track_info(info)? };
2756		State::lock().publish.track_request_accept(request, info)
2757	})
2758}
2759
2760/// Accept a track request as an audio track, the importer picking the timescale.
2761///
2762/// Consumes the request handle. Returns the same kind of media handle as
2763/// [moq_publish_audio], or a negative code on failure.
2764///
2765/// # Safety
2766/// - As [moq_publish_audio], for `config`.
2767#[unsafe(no_mangle)]
2768pub unsafe extern "C" fn moq_track_request_audio(request: u32, config: *const moq_audio_init) -> i32 {
2769	ffi::enter(move || {
2770		let request = ffi::parse_id(request)?;
2771		let audio = unsafe { parse_audio_init(config)? };
2772		State::lock().publish.track_request_audio(request, audio)
2773	})
2774}
2775
2776/// Accept a track request as a video track, the importer picking the timescale.
2777///
2778/// Consumes the request handle. Returns the same kind of media handle as
2779/// [moq_publish_video], or a negative code on failure.
2780///
2781/// # Safety
2782/// - As [moq_publish_audio], for a [moq_video_init].
2783#[unsafe(no_mangle)]
2784pub unsafe extern "C" fn moq_track_request_video(request: u32, config: *const moq_video_init) -> i32 {
2785	ffi::enter(move || {
2786		let request = ffi::parse_id(request)?;
2787		let video = unsafe { parse_video_init(config)? };
2788		State::lock().publish.track_request_video(request, video)
2789	})
2790}
2791
2792/// Reject a track request with an application error code, failing the waiting subscribers.
2793///
2794/// Consumes the request handle. Returns a zero on success, or a negative code on failure.
2795#[unsafe(no_mangle)]
2796pub extern "C" fn moq_track_request_abort(request: u32, error_code: u16) -> i32 {
2797	ffi::enter(move || {
2798		let request = ffi::parse_id(request)?;
2799		State::lock().publish.track_request_abort(request, error_code)
2800	})
2801}
2802
2803/// Free a track request without accepting it, which rejects it.
2804///
2805/// Returns a zero on success, or a negative code if the handle is unknown.
2806#[unsafe(no_mangle)]
2807pub extern "C" fn moq_track_request_free(request: u32) -> i32 {
2808	ffi::enter(move || {
2809		let request = ffi::parse_id(request)?;
2810		State::lock().publish.track_request_free(request)
2811	})
2812}
2813
2814/// The group sequence a group request asks for.
2815///
2816/// Returns a zero on success, or a negative code on failure.
2817///
2818/// # Safety
2819/// - `dst` must point at a writable `uint64_t`.
2820#[unsafe(no_mangle)]
2821pub unsafe extern "C" fn moq_group_request_sequence(request: u32, dst: *mut u64) -> i32 {
2822	ffi::enter(move || {
2823		let request = ffi::parse_id(request)?;
2824		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2825		*dst = State::lock().publish.group_request_info(request)?.0;
2826		Ok(())
2827	})
2828}
2829
2830/// The delivery priority the fetching consumer asked for.
2831///
2832/// Returns a zero on success, or a negative code on failure.
2833///
2834/// # Safety
2835/// - `dst` must point at a writable `uint8_t`.
2836#[unsafe(no_mangle)]
2837pub unsafe extern "C" fn moq_group_request_priority(request: u32, dst: *mut u8) -> i32 {
2838	ffi::enter(move || {
2839		let request = ffi::parse_id(request)?;
2840		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2841		*dst = State::lock().publish.group_request_info(request)?.1;
2842		Ok(())
2843	})
2844}
2845
2846/// The first frame of the group the fetch wants; 0 is the whole group.
2847///
2848/// [moq_group_request_accept] positions the returned producer here, so frames you
2849/// write keep the indices they have in the group rather than restarting at 0. Read
2850/// this to know which frames to fetch from storage.
2851///
2852/// Returns a zero on success, or a negative code on failure.
2853///
2854/// # Safety
2855/// - `dst` must point at a writable `uint64_t`.
2856#[unsafe(no_mangle)]
2857pub unsafe extern "C" fn moq_group_request_frame_start(request: u32, dst: *mut u64) -> i32 {
2858	ffi::enter(move || {
2859		let request = ffi::parse_id(request)?;
2860		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2861		*dst = State::lock().publish.group_request_info(request)?.2;
2862		Ok(())
2863	})
2864}
2865
2866/// Accept a group request, resolving the waiting fetches with the group you then fill.
2867///
2868/// Consumes the request handle. The returned producer starts at
2869/// [moq_group_request_frame_start], so the first frame you write lands at that
2870/// index. Returns a non-zero group handle usable with [moq_publish_group_frame]
2871/// and [moq_publish_group_finish], or a negative code on failure, including when
2872/// the group is already cached.
2873#[unsafe(no_mangle)]
2874pub extern "C" fn moq_group_request_accept(request: u32) -> i32 {
2875	ffi::enter(move || {
2876		let request = ffi::parse_id(request)?;
2877		State::lock().publish.group_request_accept(request)
2878	})
2879}
2880
2881/// Reject a group request with an application error code, failing the waiting fetches.
2882///
2883/// Consumes the request handle. Returns a zero on success, or a negative code on failure.
2884#[unsafe(no_mangle)]
2885pub extern "C" fn moq_group_request_abort(request: u32, error_code: u16) -> i32 {
2886	ffi::enter(move || {
2887		let request = ffi::parse_id(request)?;
2888		State::lock().publish.group_request_abort(request, error_code)
2889	})
2890}
2891
2892/// Free a group request without accepting it, which rejects it.
2893///
2894/// Returns a zero on success, or a negative code if the handle is unknown.
2895#[unsafe(no_mangle)]
2896pub extern "C" fn moq_group_request_free(request: u32) -> i32 {
2897	ffi::enter(move || {
2898		let request = ffi::parse_id(request)?;
2899		State::lock().publish.group_request_free(request)
2900	})
2901}
2902
2903/// Write a frame into a raw group created by [moq_publish_track_group].
2904///
2905/// The timestamp is in microseconds.
2906///
2907/// Returns a zero on success, or a negative code on failure.
2908///
2909/// # Safety
2910/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
2911#[unsafe(no_mangle)]
2912pub unsafe extern "C" fn moq_publish_group_frame(
2913	group: u32,
2914	payload: *const u8,
2915	payload_size: usize,
2916	timestamp_us: u64,
2917) -> i32 {
2918	ffi::enter(move || {
2919		let group = ffi::parse_id(group)?;
2920		let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
2921		let timestamp = moq_net::Timestamp::from_micros(timestamp_us)?;
2922		State::lock().publish.group_frame(group, timestamp, payload)
2923	})
2924}
2925
2926/// Finish a raw group. No more frames can be written.
2927///
2928/// Returns a zero on success, or a negative code on failure.
2929#[unsafe(no_mangle)]
2930pub extern "C" fn moq_publish_group_finish(group: u32) -> i32 {
2931	ffi::enter(move || {
2932		let group = ffi::parse_id(group)?;
2933		State::lock().publish.group_finish(group)
2934	})
2935}
2936
2937/// Abort a raw group with an application error code.
2938#[unsafe(no_mangle)]
2939pub extern "C" fn moq_publish_group_abort(group: u32, error_code: u16) -> i32 {
2940	ffi::enter(move || {
2941		let group = ffi::parse_id(group)?;
2942		State::lock().publish.group_abort(group, error_code)
2943	})
2944}
2945
2946/// Create a JSON snapshot track (lossy latest-value) on a broadcast.
2947///
2948/// Values published via [moq_publish_json_snapshot_update] reach subscribers as a single latest
2949/// state; a late joiner only sees the newest. The track is advertised in the broadcast's catalog
2950/// under `json.tracks.<name>` with `mode: snapshot` (and `compression: deflate` when set), and the
2951/// entry is retired when the track finishes or fails, so consumers discover it with no extra call.
2952///
2953/// Returns a non-zero handle to the JSON producer on success, or a negative code on failure,
2954/// including a mux error when the catalog already carries an entry named `name`.
2955///
2956/// # Safety
2957/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
2958#[unsafe(no_mangle)]
2959pub unsafe extern "C" fn moq_publish_json_snapshot(
2960	broadcast: u32,
2961	name: *const c_char,
2962	name_len: usize,
2963	config: *const moq_json_snapshot_config,
2964) -> i32 {
2965	ffi::enter(move || {
2966		let broadcast = ffi::parse_id(broadcast)?;
2967		let name = unsafe { ffi::parse_str(name, name_len)? };
2968		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
2969		let producer = moq_mux::json::Config::default()
2970			.with_compression(config.compression)
2971			.with_delta_ratio(config.delta_ratio);
2972		State::lock().publish.json_snapshot(broadcast, name, producer)
2973	})
2974}
2975
2976/// Publish a new value to a JSON snapshot track. `value` is a UTF-8 JSON document. A no-op if
2977/// unchanged from the previous update.
2978///
2979/// Returns a zero on success, or a negative code on failure.
2980///
2981/// # Safety
2982/// - The caller must ensure `value` is a valid pointer to `value_len` bytes.
2983#[unsafe(no_mangle)]
2984pub unsafe extern "C" fn moq_publish_json_snapshot_update(json: u32, value: *const c_char, value_len: usize) -> i32 {
2985	ffi::enter(move || {
2986		let json = ffi::parse_id(json)?;
2987		let value = unsafe { ffi::parse_slice(value.cast::<u8>(), value_len)? };
2988		let value = serde_json::from_slice(value)?;
2989		State::lock().publish.json_snapshot_update(json, value)
2990	})
2991}
2992
2993/// Finish a JSON snapshot track. No more values can be published.
2994///
2995/// Returns a zero on success, or a negative code on failure.
2996#[unsafe(no_mangle)]
2997pub extern "C" fn moq_publish_json_snapshot_finish(json: u32) -> i32 {
2998	ffi::enter(move || {
2999		let json = ffi::parse_id(json)?;
3000		State::lock().publish.json_snapshot_finish(json)
3001	})
3002}
3003
3004/// Create a JSON stream track (lossless append-log) on a broadcast.
3005///
3006/// Every record appended via [moq_publish_json_stream_append] is preserved and delivered in order.
3007/// The track is advertised in the broadcast's catalog under `json.tracks.<name>` with
3008/// `mode: stream`, for as long as the track lives.
3009///
3010/// Returns a non-zero handle to the JSON stream producer on success, or a negative code on failure,
3011/// including a mux error when the catalog already carries an entry named `name`.
3012///
3013/// # Safety
3014/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
3015#[unsafe(no_mangle)]
3016pub unsafe extern "C" fn moq_publish_json_stream(
3017	broadcast: u32,
3018	name: *const c_char,
3019	name_len: usize,
3020	config: *const moq_json_stream_config,
3021) -> i32 {
3022	ffi::enter(move || {
3023		let broadcast = ffi::parse_id(broadcast)?;
3024		let name = unsafe { ffi::parse_str(name, name_len)? };
3025		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
3026		let producer = moq_mux::json::Config::default().with_compression(config.compression);
3027		State::lock().publish.json_stream(broadcast, name, producer)
3028	})
3029}
3030
3031/// Append one record to a JSON stream track. `value` is a UTF-8 JSON document.
3032///
3033/// Returns a zero on success, or a negative code on failure.
3034///
3035/// # Safety
3036/// - The caller must ensure `value` is a valid pointer to `value_len` bytes.
3037#[unsafe(no_mangle)]
3038pub unsafe extern "C" fn moq_publish_json_stream_append(stream: u32, value: *const c_char, value_len: usize) -> i32 {
3039	ffi::enter(move || {
3040		let stream = ffi::parse_id(stream)?;
3041		let value = unsafe { ffi::parse_slice(value.cast::<u8>(), value_len)? };
3042		let value = serde_json::from_slice(value)?;
3043		State::lock().publish.json_stream_append(stream, value)
3044	})
3045}
3046
3047/// Finish a JSON stream track. No more records can be appended.
3048///
3049/// Returns a zero on success, or a negative code on failure.
3050#[unsafe(no_mangle)]
3051pub extern "C" fn moq_publish_json_stream_finish(stream: u32) -> i32 {
3052	ffi::enter(move || {
3053		let stream = ffi::parse_id(stream)?;
3054		State::lock().publish.json_stream_finish(stream)
3055	})
3056}
3057
3058/// Parse a [moq_binary_config] into the mux's binary track config.
3059///
3060/// # Safety
3061/// - `config` must be a valid pointer, and its `mime` a valid pointer to `mime_len` bytes when not NULL.
3062unsafe fn binary_config(config: *const moq_binary_config) -> Result<moq_mux::binary::Config, Error> {
3063	let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
3064	let mut binary = moq_mux::binary::Config::default().with_compression(config.compression);
3065	if let Some(mime) = unsafe { ffi::parse_str_optional(config.mime, config.mime_len)? } {
3066		binary = binary.with_mime(mime);
3067	}
3068	Ok(binary)
3069}
3070
3071/// Create a binary snapshot track (lossy latest-value) on a broadcast: each payload supersedes the
3072/// last, and a late joiner only sees the newest, e.g. the latest thumbnail of a camera.
3073///
3074/// The track is advertised in the broadcast's catalog under `binary.tracks.<name>` with
3075/// `mode: snapshot` (plus `mime` and `compression` when set), and the entry is retired when the
3076/// track finishes or fails.
3077///
3078/// Returns a non-zero handle to the binary producer on success, or a negative code on failure,
3079/// including a mux error when the catalog already carries an entry named `name`.
3080///
3081/// # Safety
3082/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
3083#[unsafe(no_mangle)]
3084pub unsafe extern "C" fn moq_publish_binary_snapshot(
3085	broadcast: u32,
3086	name: *const c_char,
3087	name_len: usize,
3088	config: *const moq_binary_config,
3089) -> i32 {
3090	ffi::enter(move || {
3091		let broadcast = ffi::parse_id(broadcast)?;
3092		let name = unsafe { ffi::parse_str(name, name_len)? };
3093		let config = unsafe { binary_config(config)? };
3094		State::lock().publish.binary_snapshot(broadcast, name, config)
3095	})
3096}
3097
3098/// Publish a new payload to a binary snapshot track, superseding the last.
3099///
3100/// Returns a zero on success, or a negative code on failure.
3101///
3102/// # Safety
3103/// - The caller must ensure `payload` is a valid pointer to `payload_len` bytes.
3104#[unsafe(no_mangle)]
3105pub unsafe extern "C" fn moq_publish_binary_snapshot_update(
3106	binary: u32,
3107	payload: *const u8,
3108	payload_len: usize,
3109) -> i32 {
3110	ffi::enter(move || {
3111		let binary = ffi::parse_id(binary)?;
3112		let payload = unsafe { ffi::parse_slice(payload, payload_len)? };
3113		State::lock().publish.binary_snapshot_update(binary, payload)
3114	})
3115}
3116
3117/// Finish a binary snapshot track and retire its catalog entry. No more payloads can be published.
3118///
3119/// Returns a zero on success, or a negative code on failure.
3120#[unsafe(no_mangle)]
3121pub extern "C" fn moq_publish_binary_snapshot_finish(binary: u32) -> i32 {
3122	ffi::enter(move || {
3123		let binary = ffi::parse_id(binary)?;
3124		State::lock().publish.binary_snapshot_finish(binary)
3125	})
3126}
3127
3128/// Create a binary stream track (lossless append-log) on a broadcast: every payload is preserved
3129/// and delivered in order.
3130///
3131/// The track is advertised in the broadcast's catalog under `binary.tracks.<name>` with
3132/// `mode: stream` (plus `mime` and `compression` when set), for as long as the track lives.
3133///
3134/// Returns a non-zero handle to the binary stream producer on success, or a negative code on
3135/// failure, including a mux error when the catalog already carries an entry named `name`.
3136///
3137/// # Safety
3138/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
3139#[unsafe(no_mangle)]
3140pub unsafe extern "C" fn moq_publish_binary_stream(
3141	broadcast: u32,
3142	name: *const c_char,
3143	name_len: usize,
3144	config: *const moq_binary_config,
3145) -> i32 {
3146	ffi::enter(move || {
3147		let broadcast = ffi::parse_id(broadcast)?;
3148		let name = unsafe { ffi::parse_str(name, name_len)? };
3149		let config = unsafe { binary_config(config)? };
3150		State::lock().publish.binary_stream(broadcast, name, config)
3151	})
3152}
3153
3154/// Append one payload to a binary stream track.
3155///
3156/// Returns a zero on success, or a negative code on failure.
3157///
3158/// # Safety
3159/// - The caller must ensure `payload` is a valid pointer to `payload_len` bytes.
3160#[unsafe(no_mangle)]
3161pub unsafe extern "C" fn moq_publish_binary_stream_append(stream: u32, payload: *const u8, payload_len: usize) -> i32 {
3162	ffi::enter(move || {
3163		let stream = ffi::parse_id(stream)?;
3164		let payload = unsafe { ffi::parse_slice(payload, payload_len)? };
3165		State::lock().publish.binary_stream_append(stream, payload)
3166	})
3167}
3168
3169/// Finish a binary stream track and retire its catalog entry. No more payloads can be appended.
3170///
3171/// Returns a zero on success, or a negative code on failure.
3172#[unsafe(no_mangle)]
3173pub extern "C" fn moq_publish_binary_stream_finish(stream: u32) -> i32 {
3174	ffi::enter(move || {
3175		let stream = ffi::parse_id(stream)?;
3176		State::lock().publish.binary_stream_finish(stream)
3177	})
3178}
3179
3180/// Create a catalog consumer for a broadcast.
3181///
3182/// `on_catalog` is invoked with a positive catalog ID for each catalog update
3183/// (usable to query video/audio track information), then exactly once more with
3184/// a terminal code: `0` (closed cleanly) or a negative error. After the terminal
3185/// (`<= 0`) callback, `on_catalog` is never called again and `user_data` is never
3186/// touched again, so release `user_data` there. The terminal callback fires even
3187/// after [moq_consume_catalog_cancel].
3188///
3189/// Returns a non-zero handle on success, or a negative code on failure.
3190///
3191/// # Safety
3192/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_catalog` callback.
3193#[unsafe(no_mangle)]
3194pub unsafe extern "C" fn moq_consume_catalog(
3195	broadcast: u32,
3196	on_catalog: ffi::moq_status_callback,
3197	user_data: *mut c_void,
3198) -> i32 {
3199	ffi::enter(move || {
3200		let broadcast = ffi::parse_id(broadcast)?;
3201		let on_catalog = unsafe { ffi::OnStatus::new(user_data, on_catalog)? };
3202		State::lock().consume.catalog(broadcast, on_catalog)
3203	})
3204}
3205
3206/// Stop a catalog consumer's background subscription.
3207///
3208/// Returns immediately: zero on success, or a negative code if already closed.
3209/// Does NOT free `user_data`; the [moq_consume_catalog] callback still fires once
3210/// more with a terminal `0` (or a negative error), which is where `user_data`
3211/// should be released. Catalog snapshots previously delivered via the callback
3212/// remain valid until freed with [moq_consume_catalog_free].
3213#[unsafe(no_mangle)]
3214pub extern "C" fn moq_consume_catalog_cancel(catalog: u32) -> i32 {
3215	ffi::enter(move || {
3216		let catalog = ffi::parse_id(catalog)?;
3217		State::lock().consume.catalog_close(catalog)
3218	})
3219}
3220
3221/// Free a catalog snapshot received via the [moq_consume_catalog] callback.
3222///
3223/// This releases the snapshot and invalidates any borrowed references (e.g. pointers
3224/// returned by [moq_consume_video_config] or [moq_consume_audio_config]).
3225///
3226/// Returns a zero on success, or a negative code on failure.
3227#[unsafe(no_mangle)]
3228pub extern "C" fn moq_consume_catalog_free(catalog: u32) -> i32 {
3229	ffi::enter(move || {
3230		let catalog = ffi::parse_id(catalog)?;
3231		State::lock().consume.catalog_free(catalog)
3232	})
3233}
3234
3235/// Query information about a video track in a catalog.
3236///
3237/// The destination is filled with the video track information. `dst->container`
3238/// says how the track's frames are wrapped; skip a rendition whose kind is
3239/// `MOQ_CONTAINER_KIND_UNKNOWN`, since this build cannot parse it.
3240///
3241/// Returns a zero on success, or a negative code on failure.
3242///
3243/// # Safety
3244/// - The caller must ensure that `dst` is a valid pointer to a [moq_video_config] struct.
3245/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
3246#[unsafe(no_mangle)]
3247pub unsafe extern "C" fn moq_consume_video_config(catalog: u32, index: u32, dst: *mut moq_video_config) -> i32 {
3248	ffi::enter(move || {
3249		let catalog = ffi::parse_id(catalog)?;
3250		let index = index as usize;
3251		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
3252		State::lock().consume.video_config(catalog, index, dst)
3253	})
3254}
3255
3256/// Query whether the publisher recommends temporarily avoiding a video rendition.
3257///
3258/// The track remains available. A false value also covers catalogs that omit the
3259/// optional field.
3260///
3261/// Returns zero on success, or a negative code on failure.
3262///
3263/// # Safety
3264/// - The caller must ensure that `dst` points to properly aligned, writable storage for a `bool`.
3265#[unsafe(no_mangle)]
3266pub unsafe extern "C" fn moq_consume_video_stalled(catalog: u32, index: u32, dst: *mut bool) -> i32 {
3267	ffi::enter(move || {
3268		let catalog = ffi::parse_id(catalog)?;
3269		if dst.is_null() {
3270			return Err(Error::InvalidPointer);
3271		}
3272
3273		let stalled = State::lock().consume.video_stalled(catalog, index as usize)?;
3274		unsafe { dst.write(stalled) };
3275		Ok(())
3276	})
3277}
3278
3279/// Query the catalog properties shared by every video rendition.
3280///
3281/// The destination is filled by value and remains valid after the catalog snapshot is freed.
3282/// Inspect each `has_*` flag before reading its value.
3283///
3284/// Returns zero on success, or a negative code on failure.
3285///
3286/// # Safety
3287/// - The caller must ensure that `dst` points to a valid [moq_video_properties].
3288#[unsafe(no_mangle)]
3289pub unsafe extern "C" fn moq_consume_video_properties(catalog: u32, dst: *mut moq_video_properties) -> i32 {
3290	ffi::enter(move || {
3291		let catalog = ffi::parse_id(catalog)?;
3292		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
3293		State::lock().consume.video_properties(catalog, dst)
3294	})
3295}
3296
3297/// Query information about an audio track in a catalog.
3298///
3299/// The destination is filled with the audio track information. `dst->container`
3300/// says how the track's frames are wrapped; skip a rendition whose kind is
3301/// `MOQ_CONTAINER_KIND_UNKNOWN`, since this build cannot parse it.
3302///
3303/// Returns a zero on success, or a negative code on failure.
3304///
3305/// # Safety
3306/// - The caller must ensure that `dst` is a valid pointer to a [moq_audio_config] struct.
3307/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
3308#[unsafe(no_mangle)]
3309pub unsafe extern "C" fn moq_consume_audio_config(catalog: u32, index: u32, dst: *mut moq_audio_config) -> i32 {
3310	ffi::enter(move || {
3311		let catalog = ffi::parse_id(catalog)?;
3312		let index = index as usize;
3313		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
3314		State::lock().consume.audio_config(catalog, index, dst)
3315	})
3316}
3317
3318/// Number of untyped application catalog sections in a catalog snapshot.
3319///
3320/// These are the top-level catalog keys beyond `video`/`audio`, carried through
3321/// verbatim. Iterate them by index with [moq_consume_catalog_section_at], or look one up
3322/// directly by name with [moq_consume_catalog_section].
3323///
3324/// Returns the count (>= 0) on success, or a negative code on failure.
3325#[unsafe(no_mangle)]
3326pub extern "C" fn moq_consume_catalog_section_count(catalog: u32) -> i32 {
3327	ffi::enter(move || {
3328		let catalog = ffi::parse_id(catalog)?;
3329		State::lock().consume.catalog_section_count(catalog)
3330	})
3331}
3332
3333/// Query an application catalog section by index, keyed by name.
3334///
3335/// Fills `dst` with the section's name and JSON value at `index`, in the range
3336/// `[0, moq_consume_catalog_section_count)`. Both pointers borrow the snapshot's storage
3337/// and stay valid until it is freed with [moq_consume_catalog_free].
3338///
3339/// Returns a zero on success, or a negative code on failure (e.g. `index` out of
3340/// range).
3341///
3342/// # Safety
3343/// - The caller must ensure that `dst` is a valid pointer to a [moq_section] struct.
3344/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
3345#[unsafe(no_mangle)]
3346pub unsafe extern "C" fn moq_consume_catalog_section_at(catalog: u32, index: u32, dst: *mut moq_section) -> i32 {
3347	ffi::enter(move || {
3348		let catalog = ffi::parse_id(catalog)?;
3349		let index = index as usize;
3350		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
3351		State::lock().consume.catalog_section_at(catalog, index, dst)
3352	})
3353}
3354
3355/// Look up an application catalog section by name.
3356///
3357/// Fills `dst` with the section's JSON value (the document to parse yourself).
3358/// The pointer borrows the snapshot's storage and stays valid until it is freed
3359/// with [moq_consume_catalog_free].
3360///
3361/// Returns a zero on success, or a negative code on failure: no section with that
3362/// name yields a not-found error.
3363///
3364/// # Safety
3365/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
3366/// - The caller must ensure that `dst` is a valid pointer to a [moq_string] struct.
3367/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
3368#[unsafe(no_mangle)]
3369pub unsafe extern "C" fn moq_consume_catalog_section(
3370	catalog: u32,
3371	name: *const c_char,
3372	name_len: usize,
3373	dst: *mut moq_string,
3374) -> i32 {
3375	ffi::enter(move || {
3376		let catalog = ffi::parse_id(catalog)?;
3377		let name = unsafe { ffi::parse_str(name, name_len)? };
3378		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
3379		State::lock().consume.catalog_section_get(catalog, name, dst)
3380	})
3381}
3382
3383/// Consume a video track from a broadcast, delivering frames in order.
3384///
3385/// - `max_age_us` controls the maximum amount of buffering allowed before skipping a GoP.
3386/// - `on_frame` is called with a positive frame ID per frame, then exactly once
3387///   more with a terminal code: `0` (closed cleanly) or a negative error. After
3388///   the terminal (`<= 0`) callback, `on_frame` is never called again and
3389///   `user_data` is never touched again, so release `user_data` there. The
3390///   terminal callback fires even after [moq_consume_video_cancel].
3391///
3392/// Returns a non-zero handle to the track on success, or a negative code on failure.
3393///
3394/// # Safety
3395/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
3396#[unsafe(no_mangle)]
3397pub unsafe extern "C" fn moq_consume_video(
3398	catalog: u32,
3399	index: u32,
3400	max_age_us: u64,
3401	on_frame: ffi::moq_status_callback,
3402	user_data: *mut c_void,
3403) -> i32 {
3404	ffi::enter(move || {
3405		let catalog = ffi::parse_id(catalog)?;
3406		let index = index as usize;
3407		let max_age = std::time::Duration::from_micros(max_age_us);
3408		let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame)? };
3409		State::lock().consume.video(catalog, index, max_age, on_frame)
3410	})
3411}
3412
3413/// Stop a video track consumer's background task.
3414///
3415/// Returns immediately: zero on success, or a negative code if already closed.
3416/// Does NOT free `user_data`; the [moq_consume_video] `on_frame` callback
3417/// still fires once more with a terminal `0` (or a negative error), which is
3418/// where `user_data` should be released.
3419#[unsafe(no_mangle)]
3420pub extern "C" fn moq_consume_video_cancel(track: u32) -> i32 {
3421	ffi::enter(move || {
3422		let track = ffi::parse_id(track)?;
3423		State::lock().consume.track_close(track)
3424	})
3425}
3426
3427/// Consume an audio track from a broadcast, emitting the frames in order.
3428///
3429/// `on_frame` is called with a positive frame ID per frame, then exactly once
3430/// more with a terminal code: `0` (closed cleanly) or a negative error. After
3431/// the terminal (`<= 0`) callback, `on_frame` is never called again and
3432/// `user_data` is never touched again, so release `user_data` there. The
3433/// terminal callback fires even after [moq_consume_audio_cancel].
3434/// The `max_age_us` parameter controls how long to wait before skipping frames.
3435///
3436/// Returns a non-zero handle to the track on success, or a negative code on failure.
3437///
3438/// # Safety
3439/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
3440#[unsafe(no_mangle)]
3441pub unsafe extern "C" fn moq_consume_audio(
3442	catalog: u32,
3443	index: u32,
3444	max_age_us: u64,
3445	on_frame: ffi::moq_status_callback,
3446	user_data: *mut c_void,
3447) -> i32 {
3448	ffi::enter(move || {
3449		let catalog = ffi::parse_id(catalog)?;
3450		let index = index as usize;
3451		let max_age = std::time::Duration::from_micros(max_age_us);
3452		let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame)? };
3453		State::lock().consume.audio(catalog, index, max_age, on_frame)
3454	})
3455}
3456
3457/// Stop an audio track consumer's background task.
3458///
3459/// Returns immediately: zero on success, or a negative code if already closed.
3460/// Does NOT free `user_data`; the [moq_consume_audio] `on_frame` callback
3461/// still fires once more with a terminal `0` (or a negative error), which is
3462/// where `user_data` should be released.
3463#[unsafe(no_mangle)]
3464pub extern "C" fn moq_consume_audio_cancel(track: u32) -> i32 {
3465	ffi::enter(move || {
3466		let track = ffi::parse_id(track)?;
3467		State::lock().consume.track_close(track)
3468	})
3469}
3470
3471/// Get a chunk of a frame's payload.
3472///
3473/// Read the payload of a frame as a single contiguous slice.
3474///
3475/// Frames are not chunked; the entire payload is delivered through `dst.payload` /
3476/// `dst.payload_size` in one call. The pointer is valid until [`moq_consume_frame_free`]
3477/// is called for this frame.
3478///
3479/// Returns a zero on success, or a negative code on failure.
3480///
3481/// # Safety
3482/// - The caller must ensure that `dst` is a valid pointer to a [moq_frame] struct.
3483#[unsafe(no_mangle)]
3484pub unsafe extern "C" fn moq_consume_frame(frame: u32, dst: *mut moq_frame) -> i32 {
3485	ffi::enter(move || {
3486		let frame = ffi::parse_id(frame)?;
3487		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
3488		State::lock().consume.frame(frame, dst)
3489	})
3490}
3491
3492/// Free a decoded frame delivered via a [moq_consume_video] or [moq_consume_audio] callback.
3493///
3494/// Returns a zero on success, or a negative code on failure.
3495#[unsafe(no_mangle)]
3496pub extern "C" fn moq_consume_frame_free(frame: u32) -> i32 {
3497	ffi::enter(move || {
3498		let frame = ffi::parse_id(frame)?;
3499		State::lock().consume.frame_close(frame)
3500	})
3501}
3502
3503/// Close a broadcast consumer and clean up its resources.
3504///
3505/// Returns a zero on success, or a negative code on failure.
3506#[unsafe(no_mangle)]
3507pub extern "C" fn moq_consume_close(consume: u32) -> i32 {
3508	ffi::enter(move || {
3509		let consume = ffi::parse_id(consume)?;
3510		State::lock().consume.close(consume)
3511	})
3512}
3513
3514/// Subscribe to a raw track by name, delivering each frame's payload as-is.
3515///
3516/// This is the counterpart to [moq_publish_track]: no catalog lookup or
3517/// container parsing. `on_frame` is called with a positive raw frame ID for each
3518/// frame in sequence order, then exactly once more with a terminal code: `0`
3519/// (closed cleanly) or a negative error. After the terminal (`<= 0`) callback,
3520/// `on_frame` is never called again and `user_data` is never touched again, so
3521/// release `user_data` there. The terminal callback fires even after
3522/// [moq_consume_track_cancel]. Read each frame with [moq_consume_track_frame] and
3523/// release it with [moq_consume_track_frame_free]. Pass NULL for `subscription`
3524/// to use moq-net defaults.
3525///
3526/// Returns a non-zero handle to the track on success, or a negative code on failure.
3527///
3528/// # Safety
3529/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
3530/// - The caller must ensure that subscription is either NULL or a valid pointer to a [moq_subscription] struct.
3531/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
3532#[unsafe(no_mangle)]
3533pub unsafe extern "C" fn moq_consume_track(
3534	broadcast: u32,
3535	name: *const c_char,
3536	name_len: usize,
3537	subscription: *const moq_subscription,
3538	on_frame: ffi::moq_status_callback,
3539	user_data: *mut c_void,
3540) -> i32 {
3541	ffi::enter(move || {
3542		let broadcast = ffi::parse_id(broadcast)?;
3543		let name = unsafe { ffi::parse_str(name, name_len)? };
3544		let subscription = unsafe { subscription.as_ref() }.map(moq_net::track::Subscription::from);
3545		let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame)? };
3546		State::lock().consume.raw_track(broadcast, name, subscription, on_frame)
3547	})
3548}
3549
3550/// Update a raw track subscription's delivery preferences.
3551///
3552/// Pass NULL for `subscription` to reset to moq-net defaults.
3553///
3554/// Returns a zero on success, or a negative code on failure.
3555///
3556/// # Safety
3557/// - The caller must ensure that subscription is either NULL or a valid pointer to a [moq_subscription] struct.
3558#[unsafe(no_mangle)]
3559pub unsafe extern "C" fn moq_consume_track_update(track: u32, subscription: *const moq_subscription) -> i32 {
3560	ffi::enter(move || {
3561		let track = ffi::parse_id(track)?;
3562		let subscription = unsafe { subscription.as_ref() }.map(moq_net::track::Subscription::from);
3563		State::lock().consume.raw_track_update(track, subscription)
3564	})
3565}
3566
3567/// Read a raw frame's payload delivered via the [moq_consume_track] callback.
3568///
3569/// Fills `dst.payload` / `dst.payload_size`; the pointer is valid until the
3570/// frame is released with [moq_consume_frame_free]. `dst.timestamp_us` is the
3571/// frame presentation timestamp in microseconds. `dst.keyframe` is reported as
3572/// false because raw tracks do not parse codec metadata.
3573///
3574/// Returns a zero on success, or a negative code on failure.
3575///
3576/// # Safety
3577/// - The caller must ensure that `dst` is a valid pointer to a [moq_frame] struct.
3578#[unsafe(no_mangle)]
3579pub unsafe extern "C" fn moq_consume_track_frame(frame: u32, dst: *mut moq_frame) -> i32 {
3580	ffi::enter(move || {
3581		let frame = ffi::parse_id(frame)?;
3582		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
3583		State::lock().consume.raw_frame(frame, dst)
3584	})
3585}
3586
3587/// Free a raw frame delivered via the [moq_consume_track] callback, releasing its payload.
3588///
3589/// Returns a zero on success, or a negative code on failure.
3590#[unsafe(no_mangle)]
3591pub extern "C" fn moq_consume_track_frame_free(frame: u32) -> i32 {
3592	ffi::enter(move || {
3593		let frame = ffi::parse_id(frame)?;
3594		State::lock().consume.raw_frame_close(frame)
3595	})
3596}
3597
3598/// Stop a raw track consumer's background task.
3599///
3600/// Returns immediately: zero on success, or a negative code if already closed.
3601/// Does NOT free `user_data`; the [moq_consume_track] `on_frame` callback still
3602/// fires once more with a terminal `0` (or a negative error), which is where
3603/// `user_data` should be released. Frames already delivered via the callback
3604/// remain valid until released with [moq_consume_track_frame_free].
3605#[unsafe(no_mangle)]
3606pub extern "C" fn moq_consume_track_cancel(track: u32) -> i32 {
3607	ffi::enter(move || {
3608		let track = ffi::parse_id(track)?;
3609		State::lock().consume.raw_track_close(track)
3610	})
3611}
3612
3613/// Subscribe to a raw track's best-effort datagrams by name.
3614///
3615/// The datagram counterpart to [moq_consume_track], on its own subscription. `on_datagram`
3616/// is called with a positive datagram ID for each datagram in arrival order, then exactly
3617/// once more with a terminal code: `0` (closed cleanly) or a negative error. After the
3618/// terminal (`<= 0`) callback, `on_datagram` is never called again and `user_data` is never
3619/// touched again, so release `user_data` there. The terminal callback fires even after
3620/// [moq_consume_datagrams_cancel]. Read each datagram with [moq_consume_datagram] and release
3621/// it with [moq_consume_datagram_free]. Datagrams arrive only over datagram-capable
3622/// transports on moq-transport or lite-05 and newer moq-lite; there is no stream fallback.
3623///
3624/// Returns a non-zero handle to the subscription on success, or a negative code on failure.
3625///
3626/// # Safety
3627/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
3628/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_datagram` callback.
3629#[unsafe(no_mangle)]
3630pub unsafe extern "C" fn moq_consume_datagrams(
3631	broadcast: u32,
3632	name: *const c_char,
3633	name_len: usize,
3634	on_datagram: ffi::moq_status_callback,
3635	user_data: *mut c_void,
3636) -> i32 {
3637	ffi::enter(move || {
3638		let broadcast = ffi::parse_id(broadcast)?;
3639		let name = unsafe { ffi::parse_str(name, name_len)? };
3640		let on_datagram = unsafe { ffi::OnStatus::new(user_data, on_datagram)? };
3641		State::lock().consume.datagram_track(broadcast, name, on_datagram)
3642	})
3643}
3644
3645/// Read a datagram delivered via the [moq_consume_datagrams] callback.
3646///
3647/// Fills `dst.payload` / `dst.payload_size` (valid until the datagram is released with
3648/// [moq_consume_datagram_free]), plus `dst.timestamp_us` and `dst.sequence`.
3649///
3650/// Returns a zero on success, or a negative code on failure.
3651///
3652/// # Safety
3653/// - The caller must ensure that `dst` is a valid pointer to a [moq_datagram] struct.
3654#[unsafe(no_mangle)]
3655pub unsafe extern "C" fn moq_consume_datagram(datagram: u32, dst: *mut moq_datagram) -> i32 {
3656	ffi::enter(move || {
3657		let datagram = ffi::parse_id(datagram)?;
3658		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
3659		State::lock().consume.datagram(datagram, dst)
3660	})
3661}
3662
3663/// Free a datagram delivered via the [moq_consume_datagrams] callback, releasing its payload.
3664///
3665/// Returns a zero on success, or a negative code on failure.
3666#[unsafe(no_mangle)]
3667pub extern "C" fn moq_consume_datagram_free(datagram: u32) -> i32 {
3668	ffi::enter(move || {
3669		let datagram = ffi::parse_id(datagram)?;
3670		State::lock().consume.datagram_close(datagram)
3671	})
3672}
3673
3674/// Stop a datagram subscription's background task.
3675///
3676/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
3677/// `user_data`; the [moq_consume_datagrams] `on_datagram` callback still fires once more with a
3678/// terminal `0` (or a negative error), which is where `user_data` should be released. Datagrams
3679/// already delivered via the callback remain valid until released with [moq_consume_datagram_free].
3680#[unsafe(no_mangle)]
3681pub extern "C" fn moq_consume_datagrams_cancel(task: u32) -> i32 {
3682	ffi::enter(move || {
3683		let task = ffi::parse_id(task)?;
3684		State::lock().consume.datagram_track_close(task)
3685	})
3686}
3687
3688/// Subscribe to a JSON snapshot track (lossy latest-value) by name.
3689///
3690/// `on_value` is called with a positive value ID for each new latest value; a consumer that
3691/// falls behind collapses the backlog and only sees the newest. It is called exactly once more
3692/// with a terminal `0` (track ended / closed) or a negative error, after which `user_data` is
3693/// never touched again, so release it there. Read each value with [moq_consume_json_value] and
3694/// release it with [moq_consume_json_value_free]. Pass the same compression the producer used.
3695///
3696/// Returns a non-zero handle to the task on success, or a negative code on failure.
3697///
3698/// # Safety
3699/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
3700/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_value` callback.
3701#[unsafe(no_mangle)]
3702pub unsafe extern "C" fn moq_consume_json_snapshot(
3703	broadcast: u32,
3704	name: *const c_char,
3705	name_len: usize,
3706	config: *const moq_json_snapshot_config,
3707	on_value: ffi::moq_status_callback,
3708	user_data: *mut c_void,
3709) -> i32 {
3710	ffi::enter(move || {
3711		let broadcast = ffi::parse_id(broadcast)?;
3712		let name = unsafe { ffi::parse_str(name, name_len)? };
3713		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
3714		let mut consumer = moq_json::snapshot::consumer::Config::default();
3715		consumer.compression = if config.compression {
3716			moq_json::Compression::Deflate
3717		} else {
3718			moq_json::Compression::None
3719		};
3720		let on_value = unsafe { ffi::OnStatus::new(user_data, on_value)? };
3721		State::lock().consume.json_snapshot(broadcast, name, consumer, on_value)
3722	})
3723}
3724
3725/// Subscribe to a JSON stream track (lossless append-log) by name.
3726///
3727/// `on_value` is called with a positive value ID for each record, in order, then once more with
3728/// a terminal `0` or negative error where `user_data` should be released. Read each value with
3729/// [moq_consume_json_value] and release it with [moq_consume_json_value_free].
3730///
3731/// Returns a non-zero handle to the task on success, or a negative code on failure.
3732///
3733/// # Safety
3734/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
3735/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_value` callback.
3736#[unsafe(no_mangle)]
3737pub unsafe extern "C" fn moq_consume_json_stream(
3738	broadcast: u32,
3739	name: *const c_char,
3740	name_len: usize,
3741	config: *const moq_json_stream_config,
3742	on_value: ffi::moq_status_callback,
3743	user_data: *mut c_void,
3744) -> i32 {
3745	ffi::enter(move || {
3746		let broadcast = ffi::parse_id(broadcast)?;
3747		let name = unsafe { ffi::parse_str(name, name_len)? };
3748		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
3749		let mut consumer = moq_json::stream::Config::default();
3750		if config.compression {
3751			consumer.compression = moq_json::Compression::Deflate;
3752		}
3753		let on_value = unsafe { ffi::OnStatus::new(user_data, on_value)? };
3754		State::lock().consume.json_stream(broadcast, name, consumer, on_value)
3755	})
3756}
3757
3758/// Read a JSON value delivered via a [moq_consume_json_snapshot] or [moq_consume_json_stream] callback.
3759///
3760/// Fills `dst.json` / `dst.json_len`; the pointer is valid until the value is released with
3761/// [moq_consume_json_value_free].
3762///
3763/// Returns a zero on success, or a negative code on failure.
3764///
3765/// # Safety
3766/// - The caller must ensure `dst` is a valid pointer to a [moq_json_value] struct.
3767#[unsafe(no_mangle)]
3768pub unsafe extern "C" fn moq_consume_json_value(value: u32, dst: *mut moq_json_value) -> i32 {
3769	ffi::enter(move || {
3770		let value = ffi::parse_id(value)?;
3771		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
3772		State::lock().consume.json_value(value, dst)
3773	})
3774}
3775
3776/// Release a JSON value delivered via a consumer callback.
3777///
3778/// Returns a zero on success, or a negative code on failure.
3779#[unsafe(no_mangle)]
3780pub extern "C" fn moq_consume_json_value_free(value: u32) -> i32 {
3781	ffi::enter(move || {
3782		let value = ffi::parse_id(value)?;
3783		State::lock().consume.json_value_close(value)
3784	})
3785}
3786
3787/// Stop a JSON consumer's background task (snapshot or stream).
3788///
3789/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
3790/// `user_data`; the `on_value` callback still fires once more with a terminal `0` (or a negative
3791/// error), which is where `user_data` should be released. Values already delivered remain valid
3792/// until released with [moq_consume_json_value_free].
3793#[unsafe(no_mangle)]
3794pub extern "C" fn moq_consume_json_cancel(task: u32) -> i32 {
3795	ffi::enter(move || {
3796		let task = ffi::parse_id(task)?;
3797		State::lock().consume.json_close(task)
3798	})
3799}