Skip to main content

moq/
api.rs

1use crate::{Error, State, ffi};
2
3use std::ffi::c_char;
4use std::ffi::c_void;
5use std::str::FromStr;
6
7use tracing::Level;
8
9/// Information about a video rendition in the catalog.
10#[repr(C)]
11#[allow(non_camel_case_types)]
12pub struct moq_video_config {
13	/// The name of the track, NOT NULL terminated.
14	pub name: *const c_char,
15	pub name_len: usize,
16
17	/// The codec of the track, NOT NULL terminated
18	pub codec: *const c_char,
19	pub codec_len: usize,
20
21	/// The description of the track, or NULL if not used.
22	/// This is codec specific, for example H264:
23	///   - NULL: annex.b encoded
24	///   - Non-NULL: AVCC encoded
25	pub description: *const u8,
26	pub description_len: usize,
27
28	/// The encoded width/height of the media, or NULL if not available
29	pub coded_width: *const u32,
30	pub coded_height: *const u32,
31}
32
33/// Catalog properties shared by every video rendition.
34///
35/// A false `has_*` flag clears that field from the next catalog rather than preserving its previous value.
36#[repr(C)]
37#[allow(non_camel_case_types)]
38#[derive(Clone, Copy, Default)]
39pub struct moq_video_properties {
40	/// Final rendered width in pixels when `has_display` is true.
41	pub display_width: u32,
42
43	/// Final rendered height in pixels when `has_display` is true.
44	pub display_height: u32,
45
46	/// Whether `display_width` and `display_height` are present.
47	pub has_display: bool,
48
49	/// Clockwise rotation in degrees when `has_rotation` is true.
50	pub rotation: f64,
51
52	/// Whether `rotation` is present.
53	pub has_rotation: bool,
54
55	/// Whether to flip horizontally after rotation when `has_flip` is true.
56	pub flip: bool,
57
58	/// Whether `flip` is present.
59	pub has_flip: bool,
60}
61
62/// Information about an audio rendition in the catalog.
63#[repr(C)]
64#[allow(non_camel_case_types)]
65pub struct moq_audio_config {
66	/// The name of the track, NOT NULL terminated
67	pub name: *const c_char,
68	pub name_len: usize,
69
70	/// The codec of the track, NOT NULL terminated
71	pub codec: *const c_char,
72	pub codec_len: usize,
73
74	/// The description of the track, or NULL if not used.
75	pub description: *const u8,
76	pub description_len: usize,
77
78	/// The sample rate of the track in Hz
79	pub sample_rate: u32,
80
81	/// The number of channels in the track
82	pub channel_count: u32,
83}
84
85/// Options for a JSON snapshot track (lossy latest-value mode).
86///
87/// The same config is passed to a producer and its consumers, but the consumer reads only
88/// `compression`; `delta_ratio` is producer-only.
89#[repr(C)]
90#[allow(non_camel_case_types)]
91pub struct moq_json_snapshot_config {
92	/// How aggressively the producer emits deltas instead of full snapshots. `0` disables deltas
93	/// (one snapshot per group); a positive value allows roughly that many snapshots' worth of
94	/// deltas before rolling. Ignored by the consumer.
95	pub delta_ratio: u32,
96
97	/// DEFLATE-compress each group. Must match on the producer and consumer.
98	pub compression: bool,
99}
100
101/// Options for a JSON stream track (lossless append-log mode).
102#[repr(C)]
103#[allow(non_camel_case_types)]
104pub struct moq_json_stream_config {
105	/// DEFLATE-compress the group. Must match on the producer and consumer.
106	pub compression: bool,
107}
108
109/// A JSON value delivered by a consumer callback.
110#[repr(C)]
111#[allow(non_camel_case_types)]
112pub struct moq_json_value {
113	/// The JSON document as UTF-8, NOT NULL terminated.
114	pub json: *const c_char,
115	pub json_len: usize,
116}
117
118/// Information about a frame of media.
119#[repr(C)]
120#[allow(non_camel_case_types)]
121pub struct moq_frame {
122	/// The payload of the frame, or NULL/0 if the stream has ended
123	pub payload: *const u8,
124	pub payload_size: usize,
125
126	/// The presentation timestamp of the frame in microseconds
127	pub timestamp_us: u64,
128
129	/// Whether the frame is a keyframe, aka the start of a new group.
130	pub keyframe: bool,
131}
132
133/// A best-effort raw track datagram delivered via [moq_consume_datagrams].
134#[repr(C)]
135#[allow(non_camel_case_types)]
136pub struct moq_datagram {
137	/// The payload of the datagram, or NULL/0 if the track has ended.
138	pub payload: *const u8,
139	pub payload_size: usize,
140
141	/// The presentation timestamp of the datagram in microseconds.
142	pub timestamp_us: u64,
143
144	/// Per-track sequence number, drawn from the same namespace as groups.
145	pub sequence: u64,
146}
147
148/// Publisher-side raw track properties.
149///
150/// A null [moq_publish_track] `info` pointer uses the moq-net defaults.
151/// A zero-initialized struct also uses those defaults, except `priority` where
152/// zero is the default itself.
153#[repr(C)]
154#[allow(non_camel_case_types)]
155pub struct moq_track_info {
156	/// Priority, used to break ties between subscriptions of equal subscriber priority.
157	pub priority: u8,
158
159	/// Whether groups are prioritized in sequence order.
160	/// Groups may always arrive out-of-order (or not at all) over the network.
161	pub ordered: bool,
162
163	/// Maximum age of a non-latest group before the publisher evicts it, in milliseconds.
164	/// The publisher-side half of `moq_subscription.latency_max_ms`.
165	pub latency_max_ms: u64,
166	/// Whether `latency_max_ms` should override the default.
167	pub latency_max_valid: bool,
168
169	/// Per-frame timescale in ticks per second.
170	pub timescale: u64,
171	/// Whether `timescale` should override the default microsecond timescale,
172	/// which matches the `timestamp_us` units used everywhere else in this ABI.
173	pub timescale_valid: bool,
174}
175
176impl TryFrom<&moq_track_info> for moq_net::track::Info {
177	type Error = Error;
178
179	fn try_from(info: &moq_track_info) -> Result<Self, Self::Error> {
180		// Raw tracks default to a microsecond timescale, matching the C ABI's
181		// timestamp_us units. An explicit timescale below overrides it.
182		let mut out = moq_net::track::Info::default()
183			.with_timescale(moq_net::Timescale::MICRO)
184			.with_priority(info.priority)
185			.with_ordered(info.ordered);
186		if info.latency_max_valid {
187			out = out.with_latency_max(std::time::Duration::from_millis(info.latency_max_ms));
188		}
189		if info.timescale_valid {
190			out = out.with_timescale(moq_net::Timescale::new(info.timescale)?);
191		}
192		Ok(out)
193	}
194}
195
196/// Subscriber-side raw track delivery preferences.
197///
198/// A null [moq_consume_track] or [moq_consume_track_update] `subscription`
199/// pointer uses the moq-net defaults.
200#[repr(C)]
201#[allow(non_camel_case_types)]
202pub struct moq_subscription {
203	/// Delivery priority. Higher values preempt lower ones under contention.
204	pub priority: u8,
205
206	/// Whether groups are prioritized in sequence order.
207	/// Groups may always arrive out-of-order (or not at all) over the network.
208	pub ordered: bool,
209
210	/// Maximum age of a non-latest group before it is skipped, in milliseconds.
211	/// Zero skips immediately. Enforced by the publisher's cache and by any local buffering.
212	pub latency_max_ms: u64,
213
214	/// First group to deliver.
215	pub group_start: u64,
216	/// Whether `group_start` is present. When false, delivery starts at the latest group.
217	pub group_start_valid: bool,
218
219	/// Last group to deliver, inclusive.
220	pub group_end: u64,
221	/// Whether `group_end` is present. When false, there is no end cap.
222	pub group_end_valid: bool,
223}
224
225impl From<&moq_subscription> for moq_net::track::Subscription {
226	fn from(subscription: &moq_subscription) -> Self {
227		let mut out = moq_net::track::Subscription::default()
228			.with_priority(subscription.priority)
229			.with_ordered(subscription.ordered)
230			.with_latency_max(std::time::Duration::from_millis(subscription.latency_max_ms));
231		if subscription.group_start_valid {
232			out = out.with_group_start(subscription.group_start);
233		}
234		if subscription.group_end_valid {
235			out = out.with_group_end(subscription.group_end);
236		}
237		out
238	}
239}
240
241/// A borrowed UTF-8 string slice, NOT NULL terminated.
242///
243/// Used to hand a C caller a JSON document that lives inside libmoq's storage.
244/// The pointer borrows that storage and is only valid until the owning resource
245/// is freed (see the function that fills it for the exact lifetime).
246#[repr(C)]
247#[allow(non_camel_case_types)]
248pub struct moq_string {
249	/// Pointer to `len` bytes of UTF-8, NOT NULL terminated.
250	pub data: *const c_char,
251	pub len: usize,
252}
253
254/// One untyped application catalog section: a name and its JSON value.
255///
256/// Both `name` and `json` are UTF-8, NOT NULL terminated, and borrow the catalog
257/// snapshot's storage. They stay valid until the snapshot is freed with
258/// [moq_consume_catalog_free]. `json` is the section's value serialized as JSON
259/// (parse it yourself); a top-level catalog key beyond `video`/`audio`.
260#[repr(C)]
261#[allow(non_camel_case_types)]
262pub struct moq_section {
263	/// The section name, NOT NULL terminated.
264	pub name: *const c_char,
265	pub name_len: usize,
266
267	/// The section value as a JSON document, NOT NULL terminated.
268	pub json: *const c_char,
269	pub json_len: usize,
270}
271
272/// Information about a broadcast announced by an origin.
273#[repr(C)]
274#[allow(non_camel_case_types)]
275pub struct moq_announced {
276	/// The path of the broadcast, NOT NULL terminated
277	pub path: *const c_char,
278	pub path_len: usize,
279
280	/// Whether the broadcast is active or has ended
281	/// This MUST toggle between true and false over the lifetime of the broadcast
282	pub active: bool,
283}
284
285/// A snapshot of connection statistics, filled in by [moq_session_stats].
286///
287/// Each metric has a `*_valid` flag: when `false`, the matching value is meaningless because
288/// the transport backend doesn't report it (a `false` flag is NOT the same as a zero value).
289/// Native QUIC reports every metric; the browser WebTransport reports few or none. Initialize
290/// the struct to zero before the call; [moq_session_stats] overwrites every field.
291#[repr(C)]
292#[allow(non_camel_case_types)]
293pub struct moq_connection_stats {
294	/// Smoothed round-trip time, in microseconds.
295	pub rtt_us: u64,
296	pub rtt_valid: bool,
297
298	/// Estimated send bandwidth from the congestion controller, in bits per second.
299	pub send_rate_bps: u64,
300	pub send_rate_valid: bool,
301
302	/// Estimated receive bandwidth from MoQ PROBE, in bits per second.
303	pub recv_rate_bps: u64,
304	pub recv_rate_valid: bool,
305
306	/// Total bytes sent, including retransmissions and overhead.
307	pub bytes_sent: u64,
308	pub bytes_sent_valid: bool,
309
310	/// Total bytes received, including duplicates and overhead.
311	pub bytes_received: u64,
312	pub bytes_received_valid: bool,
313
314	/// Total bytes lost (detected via retransmission or acknowledgement).
315	pub bytes_lost: u64,
316	pub bytes_lost_valid: bool,
317
318	/// Total datagrams sent.
319	pub packets_sent: u64,
320	pub packets_sent_valid: bool,
321
322	/// Total datagrams received.
323	pub packets_received: u64,
324	pub packets_received_valid: bool,
325
326	/// Total datagrams detected as lost.
327	pub packets_lost: u64,
328	pub packets_lost_valid: bool,
329}
330
331impl From<&moq_net::ConnectionStats> for moq_connection_stats {
332	fn from(stats: &moq_net::ConnectionStats) -> Self {
333		// An Option<u64> becomes a (value, valid) pair; absent metrics report 0/false.
334		fn split(value: Option<u64>) -> (u64, bool) {
335			(value.unwrap_or(0), value.is_some())
336		}
337
338		let (rtt_us, rtt_valid) = split(stats.rtt.map(|d| d.as_micros() as u64));
339		let (send_rate_bps, send_rate_valid) = split(stats.estimated_send_rate);
340		let (recv_rate_bps, recv_rate_valid) = split(stats.estimated_recv_rate);
341		let (bytes_sent, bytes_sent_valid) = split(stats.bytes_sent);
342		let (bytes_received, bytes_received_valid) = split(stats.bytes_received);
343		let (bytes_lost, bytes_lost_valid) = split(stats.bytes_lost);
344		let (packets_sent, packets_sent_valid) = split(stats.packets_sent);
345		let (packets_received, packets_received_valid) = split(stats.packets_received);
346		let (packets_lost, packets_lost_valid) = split(stats.packets_lost);
347
348		Self {
349			rtt_us,
350			rtt_valid,
351			send_rate_bps,
352			send_rate_valid,
353			recv_rate_bps,
354			recv_rate_valid,
355			bytes_sent,
356			bytes_sent_valid,
357			bytes_received,
358			bytes_received_valid,
359			bytes_lost,
360			bytes_lost_valid,
361			packets_sent,
362			packets_sent_valid,
363			packets_received,
364			packets_received_valid,
365			packets_lost,
366			packets_lost_valid,
367		}
368	}
369}
370
371/// Initialize the library with a log level.
372///
373/// This should be called before any other functions.
374/// The log_level is a string: "error", "warn", "info", "debug", "trace"
375///
376/// Returns a zero on success, or a negative code on failure.
377///
378/// # Safety
379/// - The caller must ensure that level is a valid pointer to level_len bytes of data.
380#[unsafe(no_mangle)]
381pub unsafe extern "C" fn moq_log_level(level: *const c_char, level_len: usize) -> i32 {
382	ffi::enter(move || {
383		match unsafe { ffi::parse_str(level, level_len)? } {
384			"" => moq_native::Log::default(),
385			level => moq_native::Log::new(Level::from_str(level)?),
386		}
387		.init()?;
388
389		Ok(())
390	})
391}
392
393/// Human-readable reason for the most recent failed call on the calling thread.
394///
395/// libmoq functions return only a negative code; this exposes the matching message
396/// (including detail the code can't carry, e.g. which URL failed to parse or why a
397/// decode failed). The string is only meaningful after a call returned a negative
398/// code; check the code first.
399///
400/// Returns a NUL-terminated, UTF-8 pointer valid until the next libmoq call **on the
401/// same thread**, or NULL if no error has been recorded on this thread. Copy it if you
402/// need it to outlive the next call. Errors delivered through status callbacks carry
403/// their code directly; read this from inside the callback to get their reason.
404#[unsafe(no_mangle)]
405pub extern "C" fn moq_error() -> *const c_char {
406	ffi::last_error_ptr()
407}
408
409/// Start establishing a connection to a MoQ server.
410///
411/// Takes origin handles, which are used for publishing and consuming broadcasts respectively.
412/// - Any broadcasts in `origin_publish` will be announced to the server.
413/// - Any broadcasts announced by the server will be available in `origin_consume`.
414/// - If an origin handle is 0, that functionality is completely disabled.
415///
416/// This may be called multiple times to connect to different servers.
417/// Origins can be shared across sessions, useful for fanout or relaying.
418///
419/// Returns a non-zero handle to the session on success, or a negative code on (immediate) failure.
420/// You should call [moq_session_close], even on error, to free up resources.
421///
422/// The session reconnects automatically with exponential backoff if the connection drops.
423/// Published broadcasts are re-announced and consumers re-subscribed on each reconnect,
424/// since the origins outlive the underlying connection.
425///
426/// `on_status` reports the session lifecycle through its status code:
427/// - `> 0` on every (re)connect, carrying the connection epoch (`1` = first connect,
428///   `2` = first reconnect, and so on), so a reconnect is distinguishable from the
429///   initial connect. May fire repeatedly. Transient disconnects are not reported.
430/// - `0` when the session is closed cleanly via [moq_session_close] (terminal).
431/// - a negative error code if reconnection permanently gives up, e.g. the backoff
432///   timeout is exceeded (terminal).
433///
434/// After a terminal (`<= 0`) status, `on_status` is never called again and `user_data`
435/// is never touched again, so that final callback is the point to release `user_data`.
436/// The terminal `0` fires even after [moq_session_close], so do not free `user_data` on
437/// the close call itself.
438///
439/// # Safety
440/// - The caller must ensure that url is a valid pointer to url_len bytes of data.
441/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_status` callback.
442#[unsafe(no_mangle)]
443pub unsafe extern "C" fn moq_session_connect(
444	url: *const c_char,
445	url_len: usize,
446	origin_publish: u32,
447	origin_consume: u32,
448	on_status: Option<extern "C" fn(user_data: *mut c_void, code: i32)>,
449	user_data: *mut c_void,
450) -> i32 {
451	ffi::enter(move || {
452		let url = ffi::parse_url(url, url_len)?;
453
454		let mut state = State::lock();
455		let publish = ffi::parse_id_optional(origin_publish)?
456			.map(|id| state.origin.get(id))
457			.transpose()?
458			.cloned();
459		let consume = ffi::parse_id_optional(origin_consume)?
460			.map(|id| state.origin.get(id))
461			.transpose()?
462			.cloned();
463
464		let on_status = unsafe { ffi::OnStatus::new(user_data, on_status) };
465		state.session.connect(url, publish, consume, on_status)
466	})
467}
468
469/// Request that a session shut down.
470///
471/// Returns immediately: zero on success, or a negative code if the session is
472/// unknown or already closing. Does NOT free `user_data`. The
473/// [moq_session_connect] `on_status` callback still fires once more with a
474/// terminal `0` (or a negative error), and that final callback is where
475/// `user_data` should be released. Safe to call from any thread, including from
476/// within `on_status`.
477#[unsafe(no_mangle)]
478pub extern "C" fn moq_session_close(session: u32) -> i32 {
479	ffi::enter(move || {
480		let session = ffi::parse_id(session)?;
481		State::lock().session.close(session)
482	})
483}
484
485/// Snapshot the current connection statistics for a session.
486///
487/// Fills `dst` with a point-in-time view of the underlying QUIC/WebTransport connection
488/// (RTT, bandwidth estimates, byte/packet counters). Each metric carries a `*_valid` flag
489/// since availability depends on the transport backend; see [moq_connection_stats].
490///
491/// Returns zero on success, or a negative code on failure: the session handle is unknown, or
492/// the session is currently reconnecting and has no live connection (in which case `dst` is
493/// left untouched). Safe to call repeatedly to poll stats over the life of the session.
494///
495/// # Safety
496/// - The caller must ensure that `dst` is a valid pointer to a [moq_connection_stats] struct.
497#[unsafe(no_mangle)]
498pub unsafe extern "C" fn moq_session_stats(session: u32, dst: *mut moq_connection_stats) -> i32 {
499	ffi::enter(move || {
500		let session = ffi::parse_id(session)?;
501		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
502		let stats = State::lock().session.stats(session)?;
503		*dst = moq_connection_stats::from(&stats);
504		Ok(())
505	})
506}
507
508/// Create an origin for publishing broadcasts.
509///
510/// Origins contain any number of broadcasts addressed by path.
511/// The same broadcast can be published to multiple origins under different paths.
512///
513/// [moq_origin_announced] can be used to discover broadcasts published to this origin.
514/// This is extremely useful for discovering what is available on the server to [moq_origin_request].
515///
516/// Returns a non-zero handle to the origin on success.
517#[unsafe(no_mangle)]
518pub extern "C" fn moq_origin_create() -> i32 {
519	ffi::enter(move || State::lock().origin.create())
520}
521
522/// Create a broadcast at `path` on an origin, for publishing media tracks.
523///
524/// The broadcast starts live: the origin announces the path so consumers can discover it,
525/// becoming visible shortly after this returns. Fill it with the `moq_publish_*` functions.
526/// Toggle discoverability with [moq_publish_set_announce]; [moq_publish_finish] unpublishes
527/// immediately.
528///
529/// Returns a non-zero broadcast handle on success, or a negative code on failure.
530///
531/// # Safety
532/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
533#[unsafe(no_mangle)]
534pub unsafe extern "C" fn moq_origin_publish(origin: u32, path: *const c_char, path_len: usize) -> i32 {
535	ffi::enter(move || {
536		let origin = ffi::parse_id(origin)?;
537		let path = unsafe { ffi::parse_str(path, path_len)? };
538
539		let mut state = State::lock();
540		let broadcast = state.origin.publish(origin, path)?;
541		state.publish.create(broadcast)
542	})
543}
544
545/// Learn about all broadcasts published to an origin.
546///
547/// `on_announce` is invoked with a positive announced ID for each broadcast,
548/// then exactly once more with a terminal code: `0` (stopped cleanly) or a
549/// negative error. After the terminal (`<= 0`) callback, `on_announce` is never
550/// called again and `user_data` is never touched again, so release `user_data`
551/// there. The terminal callback fires even after [moq_origin_announced_close].
552///
553/// - [moq_origin_announced_info] is used to query information about the broadcast.
554/// - [moq_origin_announced_free] releases each delivered announced ID once read.
555/// - [moq_origin_announced_close] is used to stop receiving announcements.
556///
557/// Returns a non-zero handle on success, or a negative code on failure.
558///
559/// # Safety
560/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_announce` callback.
561#[unsafe(no_mangle)]
562pub unsafe extern "C" fn moq_origin_announced(
563	origin: u32,
564	on_announce: Option<extern "C" fn(user_data: *mut c_void, announced: i32)>,
565	user_data: *mut c_void,
566) -> i32 {
567	ffi::enter(move || {
568		let origin = ffi::parse_id(origin)?;
569		let on_announce = unsafe { ffi::OnStatus::new(user_data, on_announce) };
570		State::lock().origin.announced(origin, on_announce)
571	})
572}
573
574/// Query information about a broadcast discovered by [moq_origin_announced].
575///
576/// The destination is filled with the broadcast information. The `path` pointer borrows
577/// the announcement's storage: copy it out before calling [moq_origin_announced_free], which
578/// invalidates it.
579///
580/// Returns a zero on success, or a negative code on failure.
581///
582/// # Safety
583/// - The caller must ensure that `dst` is a valid pointer to a [moq_announced] struct.
584#[unsafe(no_mangle)]
585pub unsafe extern "C" fn moq_origin_announced_info(announced: u32, dst: *mut moq_announced) -> i32 {
586	ffi::enter(move || {
587		let announced = ffi::parse_id(announced)?;
588		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
589		State::lock().origin.announced_info(announced, dst)
590	})
591}
592
593/// Free a single announcement delivered to a [moq_origin_announced] `on_announce` callback.
594///
595/// Each announce / unannounce event hands the callback a distinct announcement handle (read
596/// with [moq_origin_announced_info]); release it here once done to avoid leaking one per event
597/// over the life of the listener. This is per-announcement and distinct from
598/// [moq_origin_announced_close], which stops the listener itself. After freeing, any `path`
599/// pointer obtained from [moq_origin_announced_info] for this handle is dangling.
600///
601/// Returns zero on success, or a negative code if the handle is unknown.
602#[unsafe(no_mangle)]
603pub extern "C" fn moq_origin_announced_free(announced: u32) -> i32 {
604	ffi::enter(move || {
605		let announced = ffi::parse_id(announced)?;
606		State::lock().origin.announced_free(announced)
607	})
608}
609
610/// Stop receiving announcements for broadcasts published to an origin.
611///
612/// Returns immediately: zero on success, or a negative code if already closed.
613/// Does NOT free `user_data`. The [moq_origin_announced] `on_announce` callback
614/// still fires once more with a terminal `0` (or a negative error), and that
615/// final callback is where `user_data` should be released.
616#[unsafe(no_mangle)]
617pub extern "C" fn moq_origin_announced_close(announced: u32) -> i32 {
618	ffi::enter(move || {
619		let announced = ffi::parse_id(announced)?;
620		State::lock().origin.announced_close(announced)
621	})
622}
623
624/// Consume a broadcast from an origin by path, waiting until it is announced.
625///
626/// Resolves against future announcements: it waits for the announcement to arrive (e.g. over the
627/// network) and then delivers the broadcast handle via `on_broadcast`. Use it right after
628/// [moq_session_connect] to avoid racing announcement gossip. To resolve against only what is
629/// announced now (plus any dynamic fallback), use [moq_origin_request] instead.
630///
631/// `on_broadcast` is invoked with a positive broadcast handle once announced, then exactly once
632/// more with a terminal code: `0` (the wait finished, including after
633/// [moq_origin_consume_announced_close]) or a negative error. After the terminal (`<= 0`) callback,
634/// `on_broadcast` is never called again and `user_data` is never touched again, so release
635/// `user_data` there. The broadcast handle is usable with [moq_consume_catalog] / [moq_consume_track]
636/// and must be freed separately with [moq_consume_close].
637///
638/// Returns a non-zero handle to the wait on success, or a negative code on (immediate) failure.
639///
640/// # Safety
641/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
642/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_broadcast` callback.
643#[unsafe(no_mangle)]
644pub unsafe extern "C" fn moq_origin_consume_announced(
645	origin: u32,
646	path: *const c_char,
647	path_len: usize,
648	on_broadcast: Option<extern "C" fn(user_data: *mut c_void, broadcast: i32)>,
649	user_data: *mut c_void,
650) -> i32 {
651	ffi::enter(move || {
652		let origin = ffi::parse_id(origin)?;
653		let path = unsafe { ffi::parse_str(path, path_len)? }.to_string();
654		let on_broadcast = unsafe { ffi::OnStatus::new(user_data, on_broadcast) };
655		State::lock().origin.consume_announced(origin, path, on_broadcast)
656	})
657}
658
659/// Abort a wait started by [moq_origin_consume_announced].
660///
661/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
662/// `user_data`. The [moq_origin_consume_announced] `on_broadcast` callback still fires once more
663/// with a terminal `0` (or a negative error), and that final callback is where `user_data` should
664/// be released. Any broadcast handle already delivered is unaffected and must still be freed with
665/// [moq_consume_close].
666#[unsafe(no_mangle)]
667pub extern "C" fn moq_origin_consume_announced_close(task: u32) -> i32 {
668	ffi::enter(move || {
669		let task = ffi::parse_id(task)?;
670		State::lock().origin.consume_announced_close(task)
671	})
672}
673
674/// Request a broadcast from an origin by path, resolving as soon as it can be served.
675///
676/// Resolves against what is announced *now* plus any dynamic fallback, where
677/// [moq_origin_consume_announced] waits indefinitely for a future announcement: it returns an
678/// already-announced broadcast at once, otherwise falls back to a dynamic handler on the origin
679/// (if any), and fails when neither can serve the path. It does NOT wait for a later
680/// announcement.
681///
682/// `on_broadcast` is invoked with a positive broadcast handle once served, then exactly once more
683/// with a terminal code: `0` (finished, including after [moq_origin_request_close]) or a negative
684/// error. After the terminal (`<= 0`) callback, `user_data` is never touched again, so release it
685/// there. The broadcast handle is usable with [moq_consume_catalog] / [moq_consume_track] and must
686/// be freed separately with [moq_consume_close].
687///
688/// Returns a non-zero handle to the request on success, or a negative code on (immediate) failure.
689///
690/// # Safety
691/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
692/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_broadcast` callback.
693#[unsafe(no_mangle)]
694pub unsafe extern "C" fn moq_origin_request(
695	origin: u32,
696	path: *const c_char,
697	path_len: usize,
698	on_broadcast: Option<extern "C" fn(user_data: *mut c_void, broadcast: i32)>,
699	user_data: *mut c_void,
700) -> i32 {
701	ffi::enter(move || {
702		let origin = ffi::parse_id(origin)?;
703		let path = unsafe { ffi::parse_str(path, path_len)? }.to_string();
704		let on_broadcast = unsafe { ffi::OnStatus::new(user_data, on_broadcast) };
705		State::lock().origin.request(origin, path, on_broadcast)
706	})
707}
708
709/// Abort a request started by [moq_origin_request].
710///
711/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
712/// `user_data`; the [moq_origin_request] `on_broadcast` callback fires once more with a terminal
713/// code, which is where `user_data` should be released. Any broadcast handle already delivered is
714/// unaffected and must still be freed with [moq_consume_close].
715#[unsafe(no_mangle)]
716pub extern "C" fn moq_origin_request_close(task: u32) -> i32 {
717	ffi::enter(move || {
718		let task = ffi::parse_id(task)?;
719		State::lock().origin.consume_announced_close(task)
720	})
721}
722
723/// Close an origin and clean up its resources.
724///
725/// Returns a zero on success, or a negative code on failure.
726#[unsafe(no_mangle)]
727pub extern "C" fn moq_origin_close(origin: u32) -> i32 {
728	ffi::enter(move || {
729		let origin = ffi::parse_id(origin)?;
730		State::lock().origin.close(origin)
731	})
732}
733
734/// Set whether a broadcast created by [moq_origin_publish] is live: announced by its origin.
735///
736/// A non-live broadcast stays reachable by exact path for subscribes and fetches; it just is
737/// not announced. This is how a publisher goes on and off the air without tearing down the
738/// broadcast.
739///
740/// Returns a zero on success, or a negative code on failure.
741#[unsafe(no_mangle)]
742pub extern "C" fn moq_publish_set_announce(broadcast: u32, announce: bool) -> i32 {
743	ffi::enter(move || {
744		let broadcast = ffi::parse_id(broadcast)?;
745		State::lock().publish.set_announce(broadcast, announce)
746	})
747}
748
749/// Finish a broadcast and release it, ending its catalog cleanly.
750///
751/// Subscribers see a normal end of stream rather than an error, and the origin unpublishes
752/// the path immediately.
753///
754/// Returns a zero on success, or a negative code on failure.
755#[unsafe(no_mangle)]
756pub extern "C" fn moq_publish_finish(broadcast: u32) -> i32 {
757	ffi::enter(move || {
758		let broadcast = ffi::parse_id(broadcast)?;
759		State::lock().publish.finish(broadcast)
760	})
761}
762
763/// Create a new media track for a broadcast
764///
765/// All frames in [moq_publish_media_frame] must be written in decode order.
766/// The `format` controls the encoding, both of `init` and frame payloads.
767///
768/// Returns a non-zero handle to the track on success, or a negative code on failure.
769///
770/// # Safety
771/// - The caller must ensure that format is a valid pointer to format_len bytes of data.
772/// - The caller must ensure that init is a valid pointer to init_size bytes of data.
773#[unsafe(no_mangle)]
774pub unsafe extern "C" fn moq_publish_media(
775	broadcast: u32,
776	format: *const c_char,
777	format_len: usize,
778	init: *const u8,
779	init_size: usize,
780) -> i32 {
781	ffi::enter(move || {
782		let broadcast = ffi::parse_id(broadcast)?;
783		let format = unsafe { ffi::parse_str(format, format_len)? };
784		let init = unsafe { ffi::parse_slice(init, init_size)? };
785
786		State::lock().publish.media(broadcast, format, init)
787	})
788}
789
790/// Finish a media track, flushing any buffered frames. No more frames can be written.
791///
792/// Returns a zero on success, or a negative code on failure.
793#[unsafe(no_mangle)]
794pub extern "C" fn moq_publish_media_finish(export: u32) -> i32 {
795	ffi::enter(move || {
796		let export = ffi::parse_id(export)?;
797		State::lock().publish.media_finish(export)
798	})
799}
800
801/// Write data to a track.
802///
803/// The encoding of `data` depends on the track `format`.
804/// The timestamp is in microseconds.
805///
806/// Returns a zero on success, or a negative code on failure.
807///
808/// # Safety
809/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
810#[unsafe(no_mangle)]
811pub unsafe extern "C" fn moq_publish_media_frame(
812	media: u32,
813	payload: *const u8,
814	payload_size: usize,
815	timestamp_us: u64,
816) -> i32 {
817	ffi::enter(move || {
818		let media = ffi::parse_id(media)?;
819		let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
820		let timestamp = hang::container::Timestamp::from_micros(timestamp_us)?;
821		State::lock().publish.media_frame(media, payload, timestamp)
822	})
823}
824
825/// Replace the catalog properties shared by every video rendition.
826///
827/// 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.
828///
829/// Returns zero on success, or a negative code on failure.
830///
831/// # Safety
832/// - The caller must ensure that `properties` points to a valid [moq_video_properties].
833#[unsafe(no_mangle)]
834pub unsafe extern "C" fn moq_publish_video_properties(broadcast: u32, properties: *const moq_video_properties) -> i32 {
835	ffi::enter(move || {
836		let broadcast = ffi::parse_id(broadcast)?;
837		let properties = unsafe { properties.as_ref() }.ok_or(Error::InvalidPointer)?;
838
839		let mut value = hang::catalog::VideoProperties::default();
840		value.display = properties.has_display.then_some(hang::catalog::Display {
841			width: properties.display_width,
842			height: properties.display_height,
843		});
844		value.rotation = properties.has_rotation.then_some(properties.rotation);
845		value.flip = properties.has_flip.then_some(properties.flip);
846
847		State::lock().publish.video_properties(broadcast, value)
848	})
849}
850
851/// Add or replace a video rendition in a broadcast's catalog.
852///
853/// This is the producer counterpart to [moq_consume_video_config]: instead of
854/// reading a rendition out of a catalog, it writes one into the catalog of a
855/// broadcast created with [moq_origin_publish]. The rendition is keyed by
856/// `config.name`; calling this again with the same name replaces it. The
857/// updated catalog is published to subscribers automatically.
858///
859/// The struct fields are read as inputs:
860/// - `name` / `codec` are required (NOT NULL terminated) string slices.
861/// - `description` may be NULL to omit it.
862/// - `coded_width` / `coded_height` may be NULL to omit them.
863///
864/// Returns a zero on success, or a negative code on failure.
865///
866/// # Safety
867/// - The caller must ensure that `config` points to a valid [moq_video_config].
868/// - The caller must ensure each non-NULL pointer inside `config` is valid for its length.
869#[unsafe(no_mangle)]
870pub unsafe extern "C" fn moq_publish_video_config(broadcast: u32, config: *const moq_video_config) -> i32 {
871	ffi::enter(move || {
872		let broadcast = ffi::parse_id(broadcast)?;
873		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
874
875		let name = unsafe { ffi::parse_str(config.name, config.name_len)? };
876		let codec = unsafe { ffi::parse_str(config.codec, config.codec_len)? };
877		let codec = hang::catalog::VideoCodec::from_str(codec).map_err(Error::Hang)?;
878
879		let mut video = hang::catalog::VideoConfig::new(codec);
880		if !config.description.is_null() {
881			let description = unsafe { ffi::parse_slice(config.description, config.description_len)? };
882			video.description = Some(bytes::Bytes::copy_from_slice(description));
883		}
884		video.coded_width = unsafe { config.coded_width.as_ref() }.copied();
885		video.coded_height = unsafe { config.coded_height.as_ref() }.copied();
886
887		State::lock().publish.video_config(broadcast, name, video)
888	})
889}
890
891/// Add or replace an audio rendition in a broadcast's catalog.
892///
893/// This is the producer counterpart to [moq_consume_audio_config]. The rendition
894/// is keyed by `config.name`; calling this again with the same name replaces it.
895/// The updated catalog is published to subscribers automatically.
896///
897/// The struct fields are read as inputs:
898/// - `name` / `codec` are required (NOT NULL terminated) string slices.
899/// - `sample_rate` / `channel_count` are required.
900/// - `description` may be NULL to omit it.
901///
902/// Returns a zero on success, or a negative code on failure.
903///
904/// # Safety
905/// - The caller must ensure that `config` points to a valid [moq_audio_config].
906/// - The caller must ensure each non-NULL pointer inside `config` is valid for its length.
907#[unsafe(no_mangle)]
908pub unsafe extern "C" fn moq_publish_audio_config(broadcast: u32, config: *const moq_audio_config) -> i32 {
909	ffi::enter(move || {
910		let broadcast = ffi::parse_id(broadcast)?;
911		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
912
913		let name = unsafe { ffi::parse_str(config.name, config.name_len)? };
914		let codec = unsafe { ffi::parse_str(config.codec, config.codec_len)? };
915		let codec = hang::catalog::AudioCodec::from_str(codec).map_err(Error::Hang)?;
916
917		let mut audio = hang::catalog::AudioConfig::new(codec, config.sample_rate, config.channel_count);
918		if !config.description.is_null() {
919			let description = unsafe { ffi::parse_slice(config.description, config.description_len)? };
920			audio.description = Some(bytes::Bytes::copy_from_slice(description));
921		}
922
923		State::lock().publish.audio_config(broadcast, name, audio)
924	})
925}
926
927/// Remove a video rendition from a broadcast's catalog by name.
928///
929/// This is a no-op if no rendition with that name exists. The updated catalog is
930/// published to subscribers automatically.
931///
932/// Returns a zero on success, or a negative code on failure.
933///
934/// # Safety
935/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
936#[unsafe(no_mangle)]
937pub unsafe extern "C" fn moq_publish_video_remove(broadcast: u32, name: *const c_char, name_len: usize) -> i32 {
938	ffi::enter(move || {
939		let broadcast = ffi::parse_id(broadcast)?;
940		let name = unsafe { ffi::parse_str(name, name_len)? };
941		State::lock().publish.video_remove(broadcast, name)
942	})
943}
944
945/// Remove an audio rendition from a broadcast's catalog by name.
946///
947/// This is a no-op if no rendition with that name exists. The updated catalog is
948/// published to subscribers automatically.
949///
950/// Returns a zero on success, or a negative code on failure.
951///
952/// # Safety
953/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
954#[unsafe(no_mangle)]
955pub unsafe extern "C" fn moq_publish_audio_remove(broadcast: u32, name: *const c_char, name_len: usize) -> i32 {
956	ffi::enter(move || {
957		let broadcast = ffi::parse_id(broadcast)?;
958		let name = unsafe { ffi::parse_str(name, name_len)? };
959		State::lock().publish.audio_remove(broadcast, name)
960	})
961}
962
963/// Set (or replace) a top-level application catalog section by name.
964///
965/// This is the producer counterpart to [moq_consume_catalog_section] /
966/// [moq_consume_catalog_section_at]: it writes an arbitrary top-level JSON key into the
967/// catalog of a broadcast created with [moq_origin_publish], beyond the
968/// `video`/`audio` keys owned by the media pipeline. Calling it again with the
969/// same name replaces the section. The updated catalog is published to
970/// subscribers automatically.
971///
972/// `json` is a JSON document (object, array, string, ...) as `json_len` bytes of
973/// UTF-8. Returns a zero on success, or a negative code on failure: invalid JSON
974/// yields a Json error (-37); a reserved `name` (`video`/`audio`) yields a mux error.
975///
976/// # Safety
977/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
978/// - The caller must ensure that json is a valid pointer to json_len bytes of data.
979#[unsafe(no_mangle)]
980pub unsafe extern "C" fn moq_publish_catalog_section(
981	broadcast: u32,
982	name: *const c_char,
983	name_len: usize,
984	json: *const c_char,
985	json_len: usize,
986) -> i32 {
987	ffi::enter(move || {
988		let broadcast = ffi::parse_id(broadcast)?;
989		let name = unsafe { ffi::parse_str(name, name_len)? };
990		let json = unsafe { ffi::parse_str(json, json_len)? };
991		let value: serde_json::Value = serde_json::from_str(json)?;
992		State::lock().publish.catalog_section_set(broadcast, name, value)
993	})
994}
995
996/// Remove a top-level application catalog section by name.
997///
998/// This is a no-op if no section with that name exists. The updated catalog is
999/// published to subscribers automatically.
1000///
1001/// Returns a zero on success, or a negative code on failure.
1002///
1003/// # Safety
1004/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
1005#[unsafe(no_mangle)]
1006pub unsafe extern "C" fn moq_publish_catalog_section_remove(
1007	broadcast: u32,
1008	name: *const c_char,
1009	name_len: usize,
1010) -> i32 {
1011	ffi::enter(move || {
1012		let broadcast = ffi::parse_id(broadcast)?;
1013		let name = unsafe { ffi::parse_str(name, name_len)? };
1014		State::lock().publish.catalog_section_remove(broadcast, name)
1015	})
1016}
1017
1018/// Create a raw track on a broadcast for arbitrary byte payloads.
1019///
1020/// Unlike [moq_publish_media], this is the bare moq-net primitive: no
1021/// codec, container, or catalog framing. Frames written to it are delivered
1022/// as-is to subscribers using [moq_consume_track]. Use it for non-media tracks
1023/// (control channels, JSON metadata, etc.), or pair it with
1024/// [moq_publish_video_config] / [moq_publish_audio_config] to also describe the
1025/// track in the catalog. Pass NULL for `info` to use moq-net defaults.
1026///
1027/// Returns a non-zero handle to the track on success, or a negative code on failure.
1028///
1029/// # Safety
1030/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
1031/// - The caller must ensure that info is either NULL or a valid pointer to a [moq_track_info] struct.
1032#[unsafe(no_mangle)]
1033pub unsafe extern "C" fn moq_publish_track(
1034	broadcast: u32,
1035	name: *const c_char,
1036	name_len: usize,
1037	info: *const moq_track_info,
1038) -> i32 {
1039	ffi::enter(move || {
1040		let broadcast = ffi::parse_id(broadcast)?;
1041		let name = unsafe { ffi::parse_str(name, name_len)? };
1042		// Default raw tracks to a microsecond timescale even when no info is given.
1043		let info = match unsafe { info.as_ref() } {
1044			Some(info) => moq_net::track::Info::try_from(info)?,
1045			None => moq_net::track::Info::default().with_timescale(moq_net::Timescale::MICRO),
1046		};
1047		State::lock().publish.track(broadcast, name, Some(info))
1048	})
1049}
1050
1051/// Append a new group to a raw track, returning a group producer.
1052///
1053/// Groups are delivered independently and each may contain any number of frames
1054/// written via [moq_publish_group_frame]. Sequence numbers auto-increment.
1055///
1056/// Returns a non-zero handle to the group on success, or a negative code on failure.
1057#[unsafe(no_mangle)]
1058pub extern "C" fn moq_publish_track_group(track: u32) -> i32 {
1059	ffi::enter(move || {
1060		let track = ffi::parse_id(track)?;
1061		State::lock().publish.track_group(track)
1062	})
1063}
1064
1065/// Create a raw group with an explicit sequence number.
1066///
1067/// Returns a non-zero group handle on success, or a negative code on failure.
1068#[unsafe(no_mangle)]
1069pub extern "C" fn moq_publish_track_group_at(track: u32, sequence: u64) -> i32 {
1070	ffi::enter(move || {
1071		let track = ffi::parse_id(track)?;
1072		State::lock().publish.track_group_at(track, sequence)
1073	})
1074}
1075
1076/// Write a single-frame group to a raw track with a timestamp.
1077///
1078/// Convenience for the common one-frame-per-group pattern. Equivalent to
1079/// appending a group, writing one frame, and finishing it.
1080/// The timestamp is in microseconds.
1081///
1082/// Returns a zero on success, or a negative code on failure.
1083///
1084/// # Safety
1085/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
1086#[unsafe(no_mangle)]
1087pub unsafe extern "C" fn moq_publish_track_frame(
1088	track: u32,
1089	payload: *const u8,
1090	payload_size: usize,
1091	timestamp_us: u64,
1092) -> i32 {
1093	ffi::enter(move || {
1094		let track = ffi::parse_id(track)?;
1095		let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
1096		let timestamp = moq_net::Timestamp::from_micros(timestamp_us)?;
1097		State::lock().publish.track_frame(track, timestamp, payload)
1098	})
1099}
1100
1101/// Send a best-effort datagram on a raw track created by [moq_publish_track].
1102///
1103/// Takes `payload` then `timestamp_us`, matching [moq_publish_track_frame]. The payload must
1104/// be at most 1200 bytes. On success the datagram's per-track sequence number (shared with the
1105/// group namespace) is written to `out_sequence` when it is non-NULL. Datagrams are
1106/// delivered only on transports and wire versions with a datagram channel; there is no
1107/// group fallback.
1108///
1109/// Returns a zero on success, or a negative code on failure.
1110///
1111/// # Safety
1112/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
1113/// - `out_sequence` must be NULL or a valid pointer to a `uint64_t`.
1114#[unsafe(no_mangle)]
1115pub unsafe extern "C" fn moq_publish_track_datagram(
1116	track: u32,
1117	payload: *const u8,
1118	payload_size: usize,
1119	timestamp_us: u64,
1120	out_sequence: *mut u64,
1121) -> i32 {
1122	ffi::enter(move || {
1123		let track = ffi::parse_id(track)?;
1124		let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
1125		let sequence = State::lock().publish.track_datagram(track, timestamp_us, payload)?;
1126		if let Some(out) = unsafe { out_sequence.as_mut() } {
1127			*out = sequence;
1128		}
1129		Ok(())
1130	})
1131}
1132
1133/// Finish a raw track. No more groups or frames can be written.
1134///
1135/// Returns a zero on success, or a negative code on failure.
1136#[unsafe(no_mangle)]
1137pub extern "C" fn moq_publish_track_finish(track: u32) -> i32 {
1138	ffi::enter(move || {
1139		let track = ffi::parse_id(track)?;
1140		State::lock().publish.track_finish(track)
1141	})
1142}
1143
1144/// Declare a raw track's exclusive final group sequence.
1145///
1146/// Groups below `final_sequence` may still be created. Groups at or above it
1147/// are rejected. The track remains open for groups below the boundary. Call
1148/// [moq_publish_track_finish] after producing the remaining groups.
1149#[unsafe(no_mangle)]
1150pub extern "C" fn moq_publish_track_finish_at(track: u32, final_sequence: u64) -> i32 {
1151	ffi::enter(move || {
1152		let track = ffi::parse_id(track)?;
1153		State::lock().publish.track_finish_at(track, final_sequence)
1154	})
1155}
1156
1157/// Abort a raw track with an application error code.
1158#[unsafe(no_mangle)]
1159pub extern "C" fn moq_publish_track_abort(track: u32, error_code: u16) -> i32 {
1160	ffi::enter(move || {
1161		let track = ffi::parse_id(track)?;
1162		State::lock().publish.track_abort(track, error_code)
1163	})
1164}
1165
1166/// Write a frame into a raw group created by [moq_publish_track_group].
1167///
1168/// The timestamp is in microseconds.
1169///
1170/// Returns a zero on success, or a negative code on failure.
1171///
1172/// # Safety
1173/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
1174#[unsafe(no_mangle)]
1175pub unsafe extern "C" fn moq_publish_group_frame(
1176	group: u32,
1177	payload: *const u8,
1178	payload_size: usize,
1179	timestamp_us: u64,
1180) -> i32 {
1181	ffi::enter(move || {
1182		let group = ffi::parse_id(group)?;
1183		let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
1184		let timestamp = moq_net::Timestamp::from_micros(timestamp_us)?;
1185		State::lock().publish.group_frame(group, timestamp, payload)
1186	})
1187}
1188
1189/// Finish a raw group. No more frames can be written.
1190///
1191/// Returns a zero on success, or a negative code on failure.
1192#[unsafe(no_mangle)]
1193pub extern "C" fn moq_publish_group_finish(group: u32) -> i32 {
1194	ffi::enter(move || {
1195		let group = ffi::parse_id(group)?;
1196		State::lock().publish.group_finish(group)
1197	})
1198}
1199
1200/// Abort a raw group with an application error code.
1201#[unsafe(no_mangle)]
1202pub extern "C" fn moq_publish_group_abort(group: u32, error_code: u16) -> i32 {
1203	ffi::enter(move || {
1204		let group = ffi::parse_id(group)?;
1205		State::lock().publish.group_abort(group, error_code)
1206	})
1207}
1208
1209/// Create a JSON snapshot track (lossy latest-value) on a broadcast.
1210///
1211/// Values published via [moq_publish_json_snapshot_update] reach subscribers as a single latest
1212/// state; a late joiner only sees the newest. Advertise the track in the catalog with
1213/// [moq_publish_catalog_section] if consumers should discover it.
1214///
1215/// Returns a non-zero handle to the JSON producer on success, or a negative code on failure.
1216///
1217/// # Safety
1218/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
1219#[unsafe(no_mangle)]
1220pub unsafe extern "C" fn moq_publish_json_snapshot(
1221	broadcast: u32,
1222	name: *const c_char,
1223	name_len: usize,
1224	config: *const moq_json_snapshot_config,
1225) -> i32 {
1226	ffi::enter(move || {
1227		let broadcast = ffi::parse_id(broadcast)?;
1228		let name = unsafe { ffi::parse_str(name, name_len)? };
1229		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1230		let mut producer = moq_json::snapshot::ProducerConfig::default();
1231		producer.delta_ratio = config.delta_ratio;
1232		producer.compression = config.compression;
1233		State::lock().publish.json_snapshot(broadcast, name, producer)
1234	})
1235}
1236
1237/// Publish a new value to a JSON snapshot track. `value` is a UTF-8 JSON document. A no-op if
1238/// unchanged from the previous update.
1239///
1240/// Returns a zero on success, or a negative code on failure.
1241///
1242/// # Safety
1243/// - The caller must ensure `value` is a valid pointer to `value_len` bytes.
1244#[unsafe(no_mangle)]
1245pub unsafe extern "C" fn moq_publish_json_snapshot_update(json: u32, value: *const c_char, value_len: usize) -> i32 {
1246	ffi::enter(move || {
1247		let json = ffi::parse_id(json)?;
1248		let value = unsafe { ffi::parse_slice(value.cast::<u8>(), value_len)? };
1249		let value = serde_json::from_slice(value)?;
1250		State::lock().publish.json_snapshot_update(json, value)
1251	})
1252}
1253
1254/// Finish a JSON snapshot track. No more values can be published.
1255///
1256/// Returns a zero on success, or a negative code on failure.
1257#[unsafe(no_mangle)]
1258pub extern "C" fn moq_publish_json_snapshot_finish(json: u32) -> i32 {
1259	ffi::enter(move || {
1260		let json = ffi::parse_id(json)?;
1261		State::lock().publish.json_snapshot_finish(json)
1262	})
1263}
1264
1265/// Create a JSON stream track (lossless append-log) on a broadcast.
1266///
1267/// Every record appended via [moq_publish_json_stream_append] is preserved and delivered in order.
1268///
1269/// Returns a non-zero handle to the JSON stream producer on success, or a negative code on failure.
1270///
1271/// # Safety
1272/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
1273#[unsafe(no_mangle)]
1274pub unsafe extern "C" fn moq_publish_json_stream(
1275	broadcast: u32,
1276	name: *const c_char,
1277	name_len: usize,
1278	config: *const moq_json_stream_config,
1279) -> i32 {
1280	ffi::enter(move || {
1281		let broadcast = ffi::parse_id(broadcast)?;
1282		let name = unsafe { ffi::parse_str(name, name_len)? };
1283		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1284		let producer = moq_json::stream::ProducerConfig::default().with_compression(config.compression);
1285		State::lock().publish.json_stream(broadcast, name, producer)
1286	})
1287}
1288
1289/// Append one record to a JSON stream track. `value` is a UTF-8 JSON document.
1290///
1291/// Returns a zero on success, or a negative code on failure.
1292///
1293/// # Safety
1294/// - The caller must ensure `value` is a valid pointer to `value_len` bytes.
1295#[unsafe(no_mangle)]
1296pub unsafe extern "C" fn moq_publish_json_stream_append(stream: u32, value: *const c_char, value_len: usize) -> i32 {
1297	ffi::enter(move || {
1298		let stream = ffi::parse_id(stream)?;
1299		let value = unsafe { ffi::parse_slice(value.cast::<u8>(), value_len)? };
1300		let value = serde_json::from_slice(value)?;
1301		State::lock().publish.json_stream_append(stream, value)
1302	})
1303}
1304
1305/// Finish a JSON stream track. No more records can be appended.
1306///
1307/// Returns a zero on success, or a negative code on failure.
1308#[unsafe(no_mangle)]
1309pub extern "C" fn moq_publish_json_stream_finish(stream: u32) -> i32 {
1310	ffi::enter(move || {
1311		let stream = ffi::parse_id(stream)?;
1312		State::lock().publish.json_stream_finish(stream)
1313	})
1314}
1315
1316/// Create a catalog consumer for a broadcast.
1317///
1318/// `on_catalog` is invoked with a positive catalog ID for each catalog update
1319/// (usable to query video/audio track information), then exactly once more with
1320/// a terminal code: `0` (closed cleanly) or a negative error. After the terminal
1321/// (`<= 0`) callback, `on_catalog` is never called again and `user_data` is never
1322/// touched again, so release `user_data` there. The terminal callback fires even
1323/// after [moq_consume_catalog_close].
1324///
1325/// Returns a non-zero handle on success, or a negative code on failure.
1326///
1327/// # Safety
1328/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_catalog` callback.
1329#[unsafe(no_mangle)]
1330pub unsafe extern "C" fn moq_consume_catalog(
1331	broadcast: u32,
1332	on_catalog: Option<extern "C" fn(user_data: *mut c_void, catalog: i32)>,
1333	user_data: *mut c_void,
1334) -> i32 {
1335	ffi::enter(move || {
1336		let broadcast = ffi::parse_id(broadcast)?;
1337		let on_catalog = unsafe { ffi::OnStatus::new(user_data, on_catalog) };
1338		State::lock().consume.catalog(broadcast, on_catalog)
1339	})
1340}
1341
1342/// Stop a catalog consumer's background subscription.
1343///
1344/// Returns immediately: zero on success, or a negative code if already closed.
1345/// Does NOT free `user_data`; the [moq_consume_catalog] callback still fires once
1346/// more with a terminal `0` (or a negative error), which is where `user_data`
1347/// should be released. Catalog snapshots previously delivered via the callback
1348/// remain valid until freed with [moq_consume_catalog_free].
1349#[unsafe(no_mangle)]
1350pub extern "C" fn moq_consume_catalog_close(catalog: u32) -> i32 {
1351	ffi::enter(move || {
1352		let catalog = ffi::parse_id(catalog)?;
1353		State::lock().consume.catalog_close(catalog)
1354	})
1355}
1356
1357/// Free a catalog snapshot received via the [moq_consume_catalog] callback.
1358///
1359/// This releases the snapshot and invalidates any borrowed references (e.g. pointers
1360/// returned by [moq_consume_video_config] or [moq_consume_audio_config]).
1361///
1362/// Returns a zero on success, or a negative code on failure.
1363#[unsafe(no_mangle)]
1364pub extern "C" fn moq_consume_catalog_free(catalog: u32) -> i32 {
1365	ffi::enter(move || {
1366		let catalog = ffi::parse_id(catalog)?;
1367		State::lock().consume.catalog_free(catalog)
1368	})
1369}
1370
1371/// Query information about a video track in a catalog.
1372///
1373/// The destination is filled with the video track information.
1374///
1375/// Returns a zero on success, or a negative code on failure.
1376///
1377/// # Safety
1378/// - The caller must ensure that `dst` is a valid pointer to a [moq_video_config] struct.
1379/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
1380#[unsafe(no_mangle)]
1381pub unsafe extern "C" fn moq_consume_video_config(catalog: u32, index: u32, dst: *mut moq_video_config) -> i32 {
1382	ffi::enter(move || {
1383		let catalog = ffi::parse_id(catalog)?;
1384		let index = index as usize;
1385		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1386		State::lock().consume.video_config(catalog, index, dst)
1387	})
1388}
1389
1390/// Query the catalog properties shared by every video rendition.
1391///
1392/// The destination is filled by value and remains valid after the catalog snapshot is freed.
1393/// Inspect each `has_*` flag before reading its value.
1394///
1395/// Returns zero on success, or a negative code on failure.
1396///
1397/// # Safety
1398/// - The caller must ensure that `dst` points to a valid [moq_video_properties].
1399#[unsafe(no_mangle)]
1400pub unsafe extern "C" fn moq_consume_video_properties(catalog: u32, dst: *mut moq_video_properties) -> i32 {
1401	ffi::enter(move || {
1402		let catalog = ffi::parse_id(catalog)?;
1403		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1404		State::lock().consume.video_properties(catalog, dst)
1405	})
1406}
1407
1408/// Query information about an audio track in a catalog.
1409///
1410/// The destination is filled with the audio track information.
1411///
1412/// Returns a zero on success, or a negative code on failure.
1413///
1414/// # Safety
1415/// - The caller must ensure that `dst` is a valid pointer to a [moq_audio_config] struct.
1416/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
1417#[unsafe(no_mangle)]
1418pub unsafe extern "C" fn moq_consume_audio_config(catalog: u32, index: u32, dst: *mut moq_audio_config) -> i32 {
1419	ffi::enter(move || {
1420		let catalog = ffi::parse_id(catalog)?;
1421		let index = index as usize;
1422		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1423		State::lock().consume.audio_config(catalog, index, dst)
1424	})
1425}
1426
1427/// Number of untyped application catalog sections in a catalog snapshot.
1428///
1429/// These are the top-level catalog keys beyond `video`/`audio`, carried through
1430/// verbatim. Iterate them by index with [moq_consume_catalog_section_at], or look one up
1431/// directly by name with [moq_consume_catalog_section].
1432///
1433/// Returns the count (>= 0) on success, or a negative code on failure.
1434#[unsafe(no_mangle)]
1435pub extern "C" fn moq_consume_catalog_section_count(catalog: u32) -> i32 {
1436	ffi::enter(move || {
1437		let catalog = ffi::parse_id(catalog)?;
1438		State::lock().consume.catalog_section_count(catalog)
1439	})
1440}
1441
1442/// Query an application catalog section by index, keyed by name.
1443///
1444/// Fills `dst` with the section's name and JSON value at `index`, in the range
1445/// `[0, moq_consume_catalog_section_count)`. Both pointers borrow the snapshot's storage
1446/// and stay valid until it is freed with [moq_consume_catalog_free].
1447///
1448/// Returns a zero on success, or a negative code on failure (e.g. `index` out of
1449/// range).
1450///
1451/// # Safety
1452/// - The caller must ensure that `dst` is a valid pointer to a [moq_section] struct.
1453/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
1454#[unsafe(no_mangle)]
1455pub unsafe extern "C" fn moq_consume_catalog_section_at(catalog: u32, index: u32, dst: *mut moq_section) -> i32 {
1456	ffi::enter(move || {
1457		let catalog = ffi::parse_id(catalog)?;
1458		let index = index as usize;
1459		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1460		State::lock().consume.catalog_section_at(catalog, index, dst)
1461	})
1462}
1463
1464/// Look up an application catalog section by name.
1465///
1466/// Fills `dst` with the section's JSON value (the document to parse yourself).
1467/// The pointer borrows the snapshot's storage and stays valid until it is freed
1468/// with [moq_consume_catalog_free].
1469///
1470/// Returns a zero on success, or a negative code on failure: no section with that
1471/// name yields a not-found error.
1472///
1473/// # Safety
1474/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
1475/// - The caller must ensure that `dst` is a valid pointer to a [moq_string] struct.
1476/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
1477#[unsafe(no_mangle)]
1478pub unsafe extern "C" fn moq_consume_catalog_section(
1479	catalog: u32,
1480	name: *const c_char,
1481	name_len: usize,
1482	dst: *mut moq_string,
1483) -> i32 {
1484	ffi::enter(move || {
1485		let catalog = ffi::parse_id(catalog)?;
1486		let name = unsafe { ffi::parse_str(name, name_len)? };
1487		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1488		State::lock().consume.catalog_section_get(catalog, name, dst)
1489	})
1490}
1491
1492/// Consume a video track from a broadcast, delivering frames in order.
1493///
1494/// - `max_latency_ms` controls the maximum amount of buffering allowed before skipping a GoP.
1495/// - `on_frame` is called with a positive frame ID per frame, then exactly once
1496///   more with a terminal code: `0` (closed cleanly) or a negative error. After
1497///   the terminal (`<= 0`) callback, `on_frame` is never called again and
1498///   `user_data` is never touched again, so release `user_data` there. The
1499///   terminal callback fires even after [moq_consume_video_close].
1500///
1501/// Returns a non-zero handle to the track on success, or a negative code on failure.
1502///
1503/// # Safety
1504/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
1505#[unsafe(no_mangle)]
1506pub unsafe extern "C" fn moq_consume_video(
1507	catalog: u32,
1508	index: u32,
1509	max_latency_ms: u64,
1510	on_frame: Option<extern "C" fn(user_data: *mut c_void, frame: i32)>,
1511	user_data: *mut c_void,
1512) -> i32 {
1513	ffi::enter(move || {
1514		let catalog = ffi::parse_id(catalog)?;
1515		let index = index as usize;
1516		let max_latency = std::time::Duration::from_millis(max_latency_ms);
1517		let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame) };
1518		State::lock().consume.video(catalog, index, max_latency, on_frame)
1519	})
1520}
1521
1522/// Stop a video track consumer's background task.
1523///
1524/// Returns immediately: zero on success, or a negative code if already closed.
1525/// Does NOT free `user_data`; the [moq_consume_video] `on_frame` callback
1526/// still fires once more with a terminal `0` (or a negative error), which is
1527/// where `user_data` should be released.
1528#[unsafe(no_mangle)]
1529pub extern "C" fn moq_consume_video_close(track: u32) -> i32 {
1530	ffi::enter(move || {
1531		let track = ffi::parse_id(track)?;
1532		State::lock().consume.track_close(track)
1533	})
1534}
1535
1536/// Consume an audio track from a broadcast, emitting the frames in order.
1537///
1538/// `on_frame` is called with a positive frame ID per frame, then exactly once
1539/// more with a terminal code: `0` (closed cleanly) or a negative error. After
1540/// the terminal (`<= 0`) callback, `on_frame` is never called again and
1541/// `user_data` is never touched again, so release `user_data` there. The
1542/// terminal callback fires even after [moq_consume_audio_close].
1543/// The `max_latency_ms` parameter controls how long to wait before skipping frames.
1544///
1545/// Returns a non-zero handle to the track on success, or a negative code on failure.
1546///
1547/// # Safety
1548/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
1549#[unsafe(no_mangle)]
1550pub unsafe extern "C" fn moq_consume_audio(
1551	catalog: u32,
1552	index: u32,
1553	max_latency_ms: u64,
1554	on_frame: Option<extern "C" fn(user_data: *mut c_void, frame: i32)>,
1555	user_data: *mut c_void,
1556) -> i32 {
1557	ffi::enter(move || {
1558		let catalog = ffi::parse_id(catalog)?;
1559		let index = index as usize;
1560		let max_latency = std::time::Duration::from_millis(max_latency_ms);
1561		let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame) };
1562		State::lock().consume.audio(catalog, index, max_latency, on_frame)
1563	})
1564}
1565
1566/// Stop an audio track consumer's background task.
1567///
1568/// Returns immediately: zero on success, or a negative code if already closed.
1569/// Does NOT free `user_data`; the [moq_consume_audio] `on_frame` callback
1570/// still fires once more with a terminal `0` (or a negative error), which is
1571/// where `user_data` should be released.
1572#[unsafe(no_mangle)]
1573pub extern "C" fn moq_consume_audio_close(track: u32) -> i32 {
1574	ffi::enter(move || {
1575		let track = ffi::parse_id(track)?;
1576		State::lock().consume.track_close(track)
1577	})
1578}
1579
1580/// Get a chunk of a frame's payload.
1581///
1582/// Read the payload of a frame as a single contiguous slice.
1583///
1584/// Frames are not chunked; the entire payload is delivered through `dst.payload` /
1585/// `dst.payload_size` in one call. The pointer is valid until [`moq_consume_frame_free`]
1586/// is called for this frame.
1587///
1588/// Returns a zero on success, or a negative code on failure.
1589///
1590/// # Safety
1591/// - The caller must ensure that `dst` is a valid pointer to a [moq_frame] struct.
1592#[unsafe(no_mangle)]
1593pub unsafe extern "C" fn moq_consume_frame(frame: u32, dst: *mut moq_frame) -> i32 {
1594	ffi::enter(move || {
1595		let frame = ffi::parse_id(frame)?;
1596		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1597		State::lock().consume.frame(frame, dst)
1598	})
1599}
1600
1601/// Free a decoded frame delivered via a [moq_consume_video] or [moq_consume_audio] callback.
1602///
1603/// Returns a zero on success, or a negative code on failure.
1604#[unsafe(no_mangle)]
1605pub extern "C" fn moq_consume_frame_free(frame: u32) -> i32 {
1606	ffi::enter(move || {
1607		let frame = ffi::parse_id(frame)?;
1608		State::lock().consume.frame_close(frame)
1609	})
1610}
1611
1612/// Close a broadcast consumer and clean up its resources.
1613///
1614/// Returns a zero on success, or a negative code on failure.
1615#[unsafe(no_mangle)]
1616pub extern "C" fn moq_consume_close(consume: u32) -> i32 {
1617	ffi::enter(move || {
1618		let consume = ffi::parse_id(consume)?;
1619		State::lock().consume.close(consume)
1620	})
1621}
1622
1623/// Subscribe to a raw track by name, delivering each frame's payload as-is.
1624///
1625/// This is the counterpart to [moq_publish_track]: no catalog lookup or
1626/// container parsing. `on_frame` is called with a positive raw frame ID for each
1627/// frame in sequence order, then exactly once more with a terminal code: `0`
1628/// (closed cleanly) or a negative error. After the terminal (`<= 0`) callback,
1629/// `on_frame` is never called again and `user_data` is never touched again, so
1630/// release `user_data` there. The terminal callback fires even after
1631/// [moq_consume_track_close]. Read each frame with [moq_consume_track_frame] and
1632/// release it with [moq_consume_track_frame_free]. Pass NULL for `subscription`
1633/// to use moq-net defaults.
1634///
1635/// Returns a non-zero handle to the track on success, or a negative code on failure.
1636///
1637/// # Safety
1638/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
1639/// - The caller must ensure that subscription is either NULL or a valid pointer to a [moq_subscription] struct.
1640/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
1641#[unsafe(no_mangle)]
1642pub unsafe extern "C" fn moq_consume_track(
1643	broadcast: u32,
1644	name: *const c_char,
1645	name_len: usize,
1646	subscription: *const moq_subscription,
1647	on_frame: Option<extern "C" fn(user_data: *mut c_void, frame: i32)>,
1648	user_data: *mut c_void,
1649) -> i32 {
1650	ffi::enter(move || {
1651		let broadcast = ffi::parse_id(broadcast)?;
1652		let name = unsafe { ffi::parse_str(name, name_len)? };
1653		let subscription = unsafe { subscription.as_ref() }.map(moq_net::track::Subscription::from);
1654		let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame) };
1655		State::lock().consume.raw_track(broadcast, name, subscription, on_frame)
1656	})
1657}
1658
1659/// Update a raw track subscription's delivery preferences.
1660///
1661/// Pass NULL for `subscription` to reset to moq-net defaults.
1662///
1663/// Returns a zero on success, or a negative code on failure.
1664///
1665/// # Safety
1666/// - The caller must ensure that subscription is either NULL or a valid pointer to a [moq_subscription] struct.
1667#[unsafe(no_mangle)]
1668pub unsafe extern "C" fn moq_consume_track_update(track: u32, subscription: *const moq_subscription) -> i32 {
1669	ffi::enter(move || {
1670		let track = ffi::parse_id(track)?;
1671		let subscription = unsafe { subscription.as_ref() }.map(moq_net::track::Subscription::from);
1672		State::lock().consume.raw_track_update(track, subscription)
1673	})
1674}
1675
1676/// Read a raw frame's payload delivered via the [moq_consume_track] callback.
1677///
1678/// Fills `dst.payload` / `dst.payload_size`; the pointer is valid until the
1679/// frame is released with [moq_consume_frame_free]. `dst.timestamp_us` is the
1680/// frame presentation timestamp in microseconds. `dst.keyframe` is reported as
1681/// false because raw tracks do not parse codec metadata.
1682///
1683/// Returns a zero on success, or a negative code on failure.
1684///
1685/// # Safety
1686/// - The caller must ensure that `dst` is a valid pointer to a [moq_frame] struct.
1687#[unsafe(no_mangle)]
1688pub unsafe extern "C" fn moq_consume_track_frame(frame: u32, dst: *mut moq_frame) -> i32 {
1689	ffi::enter(move || {
1690		let frame = ffi::parse_id(frame)?;
1691		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1692		State::lock().consume.raw_frame(frame, dst)
1693	})
1694}
1695
1696/// Free a raw frame delivered via the [moq_consume_track] callback, releasing its payload.
1697///
1698/// Returns a zero on success, or a negative code on failure.
1699#[unsafe(no_mangle)]
1700pub extern "C" fn moq_consume_track_frame_free(frame: u32) -> i32 {
1701	ffi::enter(move || {
1702		let frame = ffi::parse_id(frame)?;
1703		State::lock().consume.raw_frame_close(frame)
1704	})
1705}
1706
1707/// Stop a raw track consumer's background task.
1708///
1709/// Returns immediately: zero on success, or a negative code if already closed.
1710/// Does NOT free `user_data`; the [moq_consume_track] `on_frame` callback still
1711/// fires once more with a terminal `0` (or a negative error), which is where
1712/// `user_data` should be released. Frames already delivered via the callback
1713/// remain valid until released with [moq_consume_track_frame_free].
1714#[unsafe(no_mangle)]
1715pub extern "C" fn moq_consume_track_close(track: u32) -> i32 {
1716	ffi::enter(move || {
1717		let track = ffi::parse_id(track)?;
1718		State::lock().consume.raw_track_close(track)
1719	})
1720}
1721
1722/// Subscribe to a raw track's best-effort datagrams by name.
1723///
1724/// The datagram counterpart to [moq_consume_track], on its own subscription. `on_datagram`
1725/// is called with a positive datagram ID for each datagram in arrival order, then exactly
1726/// once more with a terminal code: `0` (closed cleanly) or a negative error. After the
1727/// terminal (`<= 0`) callback, `on_datagram` is never called again and `user_data` is never
1728/// touched again, so release `user_data` there. The terminal callback fires even after
1729/// [moq_consume_datagrams_close]. Read each datagram with [moq_consume_datagram] and release
1730/// it with [moq_consume_datagram_free]. Datagrams arrive only over datagram-capable
1731/// transports and lite-05 or newer moq-lite; there is no stream fallback.
1732///
1733/// Returns a non-zero handle to the subscription on success, or a negative code on failure.
1734///
1735/// # Safety
1736/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
1737/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_datagram` callback.
1738#[unsafe(no_mangle)]
1739pub unsafe extern "C" fn moq_consume_datagrams(
1740	broadcast: u32,
1741	name: *const c_char,
1742	name_len: usize,
1743	on_datagram: Option<extern "C" fn(user_data: *mut c_void, datagram: i32)>,
1744	user_data: *mut c_void,
1745) -> i32 {
1746	ffi::enter(move || {
1747		let broadcast = ffi::parse_id(broadcast)?;
1748		let name = unsafe { ffi::parse_str(name, name_len)? };
1749		let on_datagram = unsafe { ffi::OnStatus::new(user_data, on_datagram) };
1750		State::lock().consume.datagram_track(broadcast, name, on_datagram)
1751	})
1752}
1753
1754/// Read a datagram delivered via the [moq_consume_datagrams] callback.
1755///
1756/// Fills `dst.payload` / `dst.payload_size` (valid until the datagram is released with
1757/// [moq_consume_datagram_free]), plus `dst.timestamp_us` and `dst.sequence`.
1758///
1759/// Returns a zero on success, or a negative code on failure.
1760///
1761/// # Safety
1762/// - The caller must ensure that `dst` is a valid pointer to a [moq_datagram] struct.
1763#[unsafe(no_mangle)]
1764pub unsafe extern "C" fn moq_consume_datagram(datagram: u32, dst: *mut moq_datagram) -> i32 {
1765	ffi::enter(move || {
1766		let datagram = ffi::parse_id(datagram)?;
1767		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1768		State::lock().consume.datagram(datagram, dst)
1769	})
1770}
1771
1772/// Free a datagram delivered via the [moq_consume_datagrams] callback, releasing its payload.
1773///
1774/// Returns a zero on success, or a negative code on failure.
1775#[unsafe(no_mangle)]
1776pub extern "C" fn moq_consume_datagram_free(datagram: u32) -> i32 {
1777	ffi::enter(move || {
1778		let datagram = ffi::parse_id(datagram)?;
1779		State::lock().consume.datagram_close(datagram)
1780	})
1781}
1782
1783/// Stop a datagram subscription's background task.
1784///
1785/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
1786/// `user_data`; the [moq_consume_datagrams] `on_datagram` callback still fires once more with a
1787/// terminal `0` (or a negative error), which is where `user_data` should be released. Datagrams
1788/// already delivered via the callback remain valid until released with [moq_consume_datagram_free].
1789#[unsafe(no_mangle)]
1790pub extern "C" fn moq_consume_datagrams_close(task: u32) -> i32 {
1791	ffi::enter(move || {
1792		let task = ffi::parse_id(task)?;
1793		State::lock().consume.datagram_track_close(task)
1794	})
1795}
1796
1797/// Subscribe to a JSON snapshot track (lossy latest-value) by name.
1798///
1799/// `on_value` is called with a positive value ID for each new latest value; a consumer that
1800/// falls behind collapses the backlog and only sees the newest. It is called exactly once more
1801/// with a terminal `0` (track ended / closed) or a negative error, after which `user_data` is
1802/// never touched again, so release it there. Read each value with [moq_consume_json_value] and
1803/// release it with [moq_consume_json_value_free]. Pass the same compression the producer used.
1804///
1805/// Returns a non-zero handle to the task on success, or a negative code on failure.
1806///
1807/// # Safety
1808/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
1809/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_value` callback.
1810#[unsafe(no_mangle)]
1811pub unsafe extern "C" fn moq_consume_json_snapshot(
1812	broadcast: u32,
1813	name: *const c_char,
1814	name_len: usize,
1815	config: *const moq_json_snapshot_config,
1816	on_value: Option<extern "C" fn(user_data: *mut c_void, value: i32)>,
1817	user_data: *mut c_void,
1818) -> i32 {
1819	ffi::enter(move || {
1820		let broadcast = ffi::parse_id(broadcast)?;
1821		let name = unsafe { ffi::parse_str(name, name_len)? };
1822		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1823		let mut consumer = moq_json::snapshot::ConsumerConfig::default();
1824		consumer.compression = config.compression;
1825		let on_value = unsafe { ffi::OnStatus::new(user_data, on_value) };
1826		State::lock().consume.json_snapshot(broadcast, name, consumer, on_value)
1827	})
1828}
1829
1830/// Subscribe to a JSON stream track (lossless append-log) by name.
1831///
1832/// `on_value` is called with a positive value ID for each record, in order, then once more with
1833/// a terminal `0` or negative error where `user_data` should be released. Read each value with
1834/// [moq_consume_json_value] and release it with [moq_consume_json_value_free].
1835///
1836/// Returns a non-zero handle to the task on success, or a negative code on failure.
1837///
1838/// # Safety
1839/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
1840/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_value` callback.
1841#[unsafe(no_mangle)]
1842pub unsafe extern "C" fn moq_consume_json_stream(
1843	broadcast: u32,
1844	name: *const c_char,
1845	name_len: usize,
1846	config: *const moq_json_stream_config,
1847	on_value: Option<extern "C" fn(user_data: *mut c_void, value: i32)>,
1848	user_data: *mut c_void,
1849) -> i32 {
1850	ffi::enter(move || {
1851		let broadcast = ffi::parse_id(broadcast)?;
1852		let name = unsafe { ffi::parse_str(name, name_len)? };
1853		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1854		let consumer = moq_json::stream::ConsumerConfig::default().with_compression(config.compression);
1855		let on_value = unsafe { ffi::OnStatus::new(user_data, on_value) };
1856		State::lock().consume.json_stream(broadcast, name, consumer, on_value)
1857	})
1858}
1859
1860/// Read a JSON value delivered via a [moq_consume_json_snapshot] or [moq_consume_json_stream] callback.
1861///
1862/// Fills `dst.json` / `dst.json_len`; the pointer is valid until the value is released with
1863/// [moq_consume_json_value_free].
1864///
1865/// Returns a zero on success, or a negative code on failure.
1866///
1867/// # Safety
1868/// - The caller must ensure `dst` is a valid pointer to a [moq_json_value] struct.
1869#[unsafe(no_mangle)]
1870pub unsafe extern "C" fn moq_consume_json_value(value: u32, dst: *mut moq_json_value) -> i32 {
1871	ffi::enter(move || {
1872		let value = ffi::parse_id(value)?;
1873		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1874		State::lock().consume.json_value(value, dst)
1875	})
1876}
1877
1878/// Release a JSON value delivered via a consumer callback.
1879///
1880/// Returns a zero on success, or a negative code on failure.
1881#[unsafe(no_mangle)]
1882pub extern "C" fn moq_consume_json_value_free(value: u32) -> i32 {
1883	ffi::enter(move || {
1884		let value = ffi::parse_id(value)?;
1885		State::lock().consume.json_value_close(value)
1886	})
1887}
1888
1889/// Stop a JSON consumer's background task (snapshot or stream).
1890///
1891/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
1892/// `user_data`; the `on_value` callback still fires once more with a terminal `0` (or a negative
1893/// error), which is where `user_data` should be released. Values already delivered remain valid
1894/// until released with [moq_consume_json_value_free].
1895#[unsafe(no_mangle)]
1896pub extern "C" fn moq_consume_json_close(task: u32) -> i32 {
1897	ffi::enter(move || {
1898		let task = ffi::parse_id(task)?;
1899		State::lock().consume.json_close(task)
1900	})
1901}