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