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/// Information about an audio rendition in the catalog.
34#[repr(C)]
35#[allow(non_camel_case_types)]
36pub struct moq_audio_config {
37	/// The name of the track, NOT NULL terminated
38	pub name: *const c_char,
39	pub name_len: usize,
40
41	/// The codec of the track, NOT NULL terminated
42	pub codec: *const c_char,
43	pub codec_len: usize,
44
45	/// The description of the track, or NULL if not used.
46	pub description: *const u8,
47	pub description_len: usize,
48
49	/// The sample rate of the track in Hz
50	pub sample_rate: u32,
51
52	/// The number of channels in the track
53	pub channel_count: u32,
54}
55
56/// An untyped application section in the catalog: a top-level key beyond `video`/`audio`.
57#[repr(C)]
58#[allow(non_camel_case_types)]
59pub struct moq_section {
60	/// The section name, NOT NULL terminated.
61	pub name: *const c_char,
62	pub name_len: usize,
63
64	/// The section value as a UTF-8 JSON document, NOT NULL terminated.
65	pub json: *const c_char,
66	pub json_len: usize,
67}
68
69/// Options for a JSON snapshot track (lossy latest-value mode).
70///
71/// The same config is passed to a producer and its consumers, but the consumer reads only
72/// `compression`; `delta_ratio` is producer-only.
73#[repr(C)]
74#[allow(non_camel_case_types)]
75pub struct moq_json_config {
76	/// How aggressively the producer emits deltas instead of full snapshots. `0` disables deltas
77	/// (one snapshot per group); a positive value allows roughly that many snapshots' worth of
78	/// deltas before rolling. Ignored by the consumer.
79	pub delta_ratio: u32,
80
81	/// DEFLATE-compress each group. Must match on the producer and consumer.
82	pub compression: bool,
83}
84
85/// Options for a JSON stream track (lossless append-log mode).
86#[repr(C)]
87#[allow(non_camel_case_types)]
88pub struct moq_json_stream_config {
89	/// DEFLATE-compress the group. Must match on the producer and consumer.
90	pub compression: bool,
91}
92
93/// A JSON value delivered by a consumer callback.
94#[repr(C)]
95#[allow(non_camel_case_types)]
96pub struct moq_json_value {
97	/// The JSON document as UTF-8, NOT NULL terminated.
98	pub json: *const c_char,
99	pub json_len: usize,
100}
101
102/// Information about a frame of media.
103#[repr(C)]
104#[allow(non_camel_case_types)]
105pub struct moq_frame {
106	/// The payload of the frame, or NULL/0 if the stream has ended
107	pub payload: *const u8,
108	pub payload_size: usize,
109
110	/// The presentation timestamp of the frame in microseconds
111	pub timestamp_us: u64,
112
113	/// Whether the frame is a keyframe, aka the start of a new group.
114	pub keyframe: bool,
115}
116
117/// Information about a broadcast announced by an origin.
118#[repr(C)]
119#[allow(non_camel_case_types)]
120pub struct moq_announced {
121	/// The path of the broadcast, NOT NULL terminated
122	pub path: *const c_char,
123	pub path_len: usize,
124
125	/// Whether the broadcast is active or has ended
126	/// This MUST toggle between true and false over the lifetime of the broadcast
127	pub active: bool,
128}
129
130/// Initialize the library with a log level.
131///
132/// This should be called before any other functions.
133/// The log_level is a string: "error", "warn", "info", "debug", "trace"
134///
135/// Returns a zero on success, or a negative code on failure.
136///
137/// # Safety
138/// - The caller must ensure that level is a valid pointer to level_len bytes of data.
139#[unsafe(no_mangle)]
140pub unsafe extern "C" fn moq_log_level(level: *const c_char, level_len: usize) -> i32 {
141	ffi::enter(move || {
142		match unsafe { ffi::parse_str(level, level_len)? } {
143			"" => moq_native::Log::default(),
144			level => moq_native::Log::new(Level::from_str(level)?),
145		}
146		.init()?;
147
148		Ok(())
149	})
150}
151
152/// Human-readable reason for the most recent failed call on the calling thread.
153///
154/// libmoq functions return only a negative code; this exposes the matching message
155/// (including detail the code can't carry, e.g. which URL failed to parse or why a
156/// decode failed). The string is only meaningful after a call returned a negative
157/// code; check the code first.
158///
159/// Returns a NUL-terminated, UTF-8 pointer valid until the next libmoq call **on the
160/// same thread**, or NULL if no error has been recorded on this thread. Copy it if you
161/// need it to outlive the next call. Errors delivered through status callbacks carry
162/// their code directly; read this from inside the callback to get their reason.
163#[unsafe(no_mangle)]
164pub extern "C" fn moq_error() -> *const c_char {
165	ffi::last_error_ptr()
166}
167
168/// Start establishing a connection to a MoQ server.
169///
170/// Takes origin handles, which are used for publishing and consuming broadcasts respectively.
171/// - Any broadcasts in `origin_publish` will be announced to the server.
172/// - Any broadcasts announced by the server will be available in `origin_consume`.
173/// - If an origin handle is 0, that functionality is completely disabled.
174///
175/// This may be called multiple times to connect to different servers.
176/// Origins can be shared across sessions, useful for fanout or relaying.
177///
178/// Returns a non-zero handle to the session on success, or a negative code on (immediate) failure.
179/// You should call [moq_session_close], even on error, to free up resources.
180///
181/// The session reconnects automatically with exponential backoff if the connection drops.
182/// Published broadcasts are re-announced and consumers re-subscribed on each reconnect,
183/// since the origins outlive the underlying connection.
184///
185/// `on_status` reports the session lifecycle through its status code:
186/// - `> 0` on every (re)connect, carrying the connection epoch (`1` = first connect,
187///   `2` = first reconnect, and so on), so a reconnect is distinguishable from the
188///   initial connect. May fire repeatedly. Transient disconnects are not reported.
189/// - `0` when the session is closed cleanly via [moq_session_close] (terminal).
190/// - a negative error code if reconnection permanently gives up, e.g. the backoff
191///   timeout is exceeded (terminal).
192///
193/// After a terminal (`<= 0`) status, `on_status` is never called again and `user_data`
194/// is never touched again, so that final callback is the point to release `user_data`.
195/// The terminal `0` fires even after [moq_session_close], so do not free `user_data` on
196/// the close call itself.
197///
198/// # Safety
199/// - The caller must ensure that url is a valid pointer to url_len bytes of data.
200/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_status` callback.
201#[unsafe(no_mangle)]
202pub unsafe extern "C" fn moq_session_connect(
203	url: *const c_char,
204	url_len: usize,
205	origin_publish: u32,
206	origin_consume: u32,
207	on_status: Option<extern "C" fn(user_data: *mut c_void, code: i32)>,
208	user_data: *mut c_void,
209) -> i32 {
210	ffi::enter(move || {
211		let url = ffi::parse_url(url, url_len)?;
212
213		let mut state = State::lock();
214		let publish = ffi::parse_id_optional(origin_publish)?
215			.map(|id| state.origin.get(id))
216			.transpose()?
217			.map(|origin: &moq_net::OriginProducer| origin.consume());
218		let consume = ffi::parse_id_optional(origin_consume)?
219			.map(|id| state.origin.get(id))
220			.transpose()?
221			.cloned();
222
223		let on_status = unsafe { ffi::OnStatus::new(user_data, on_status) };
224		state.session.connect(url, publish, consume, on_status)
225	})
226}
227
228/// Request that a session shut down.
229///
230/// Returns immediately: zero on success, or a negative code if the session is
231/// unknown or already closing. Does NOT free `user_data`. The
232/// [moq_session_connect] `on_status` callback still fires once more with a
233/// terminal `0` (or a negative error), and that final callback is where
234/// `user_data` should be released. Safe to call from any thread, including from
235/// within `on_status`.
236#[unsafe(no_mangle)]
237pub extern "C" fn moq_session_close(session: u32) -> i32 {
238	ffi::enter(move || {
239		let session = ffi::parse_id(session)?;
240		State::lock().session.close(session)
241	})
242}
243
244/// Create an origin for publishing broadcasts.
245///
246/// Origins contain any number of broadcasts addressed by path.
247/// The same broadcast can be published to multiple origins under different paths.
248///
249/// [moq_origin_announced] can be used to discover broadcasts published to this origin.
250/// This is extremely useful for discovering what is available on the server to [moq_origin_consume].
251///
252/// Returns a non-zero handle to the origin on success.
253#[unsafe(no_mangle)]
254pub extern "C" fn moq_origin_create() -> i32 {
255	ffi::enter(move || State::lock().origin.create())
256}
257
258/// Publish a broadcast to an origin.
259///
260/// The broadcast will be announced to any origin consumers, such as over the network.
261///
262/// Returns a zero on success, or a negative code on failure.
263///
264/// # Safety
265/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
266#[unsafe(no_mangle)]
267pub unsafe extern "C" fn moq_origin_publish(origin: u32, path: *const c_char, path_len: usize, broadcast: u32) -> i32 {
268	ffi::enter(move || {
269		let origin = ffi::parse_id(origin)?;
270		let path = unsafe { ffi::parse_str(path, path_len)? };
271		let broadcast = ffi::parse_id(broadcast)?;
272
273		let mut state = State::lock();
274		let broadcast = state.publish.get(broadcast)?.consume();
275		state.origin.publish(origin, path, broadcast)
276	})
277}
278
279/// Learn about all broadcasts published to an origin.
280///
281/// `on_announce` is invoked with a positive announced ID for each broadcast,
282/// then exactly once more with a terminal code: `0` (stopped cleanly) or a
283/// negative error. After the terminal (`<= 0`) callback, `on_announce` is never
284/// called again and `user_data` is never touched again, so release `user_data`
285/// there. The terminal callback fires even after [moq_origin_announced_close].
286///
287/// - [moq_origin_announced_info] is used to query information about the broadcast.
288/// - [moq_origin_announced_close] is used to stop receiving announcements.
289///
290/// Returns a non-zero handle on success, or a negative code on failure.
291///
292/// # Safety
293/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_announce` callback.
294#[unsafe(no_mangle)]
295pub unsafe extern "C" fn moq_origin_announced(
296	origin: u32,
297	on_announce: Option<extern "C" fn(user_data: *mut c_void, announced: i32)>,
298	user_data: *mut c_void,
299) -> i32 {
300	ffi::enter(move || {
301		let origin = ffi::parse_id(origin)?;
302		let on_announce = unsafe { ffi::OnStatus::new(user_data, on_announce) };
303		State::lock().origin.announced(origin, on_announce)
304	})
305}
306
307/// Query information about a broadcast discovered by [moq_origin_announced].
308///
309/// The destination is filled with the broadcast information.
310///
311/// Returns a zero on success, or a negative code on failure.
312///
313/// # Safety
314/// - The caller must ensure that `dst` is a valid pointer to a [moq_announced] struct.
315#[unsafe(no_mangle)]
316pub unsafe extern "C" fn moq_origin_announced_info(announced: u32, dst: *mut moq_announced) -> i32 {
317	ffi::enter(move || {
318		let announced = ffi::parse_id(announced)?;
319		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
320		State::lock().origin.announced_info(announced, dst)
321	})
322}
323
324/// Stop receiving announcements for broadcasts published to an origin.
325///
326/// Returns immediately: zero on success, or a negative code if already closed.
327/// Does NOT free `user_data`. The [moq_origin_announced] `on_announce` callback
328/// still fires once more with a terminal `0` (or a negative error), and that
329/// final callback is where `user_data` should be released.
330#[unsafe(no_mangle)]
331pub extern "C" fn moq_origin_announced_close(announced: u32) -> i32 {
332	ffi::enter(move || {
333		let announced = ffi::parse_id(announced)?;
334		State::lock().origin.announced_close(announced)
335	})
336}
337
338/// Consume a broadcast from an origin by path.
339///
340/// Returns a non-zero handle to the broadcast on success, or a negative code on failure.
341///
342/// # Safety
343/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
344#[unsafe(no_mangle)]
345pub unsafe extern "C" fn moq_origin_consume(origin: u32, path: *const c_char, path_len: usize) -> i32 {
346	ffi::enter(move || {
347		let origin = ffi::parse_id(origin)?;
348		let path = unsafe { ffi::parse_str(path, path_len)? };
349
350		let mut state = State::lock();
351		let broadcast = state.origin.consume(origin, path)?;
352		state.consume.start(broadcast)
353	})
354}
355
356/// Consume a broadcast from an origin by path, waiting until it is announced.
357///
358/// Unlike [moq_origin_consume], which fails immediately with a not-found code when the broadcast
359/// has not been announced yet, this waits for the announcement to arrive (e.g. over the network)
360/// and then delivers the broadcast handle via `on_broadcast`. Use it right after [moq_session_connect]
361/// to avoid racing announcement gossip, instead of polling [moq_origin_consume] in a retry loop.
362///
363/// `on_broadcast` is invoked with a positive broadcast handle once announced, then exactly once
364/// more with a terminal code: `0` (the wait finished, including after
365/// [moq_origin_consume_announced_close]) or a negative error. After the terminal (`<= 0`) callback,
366/// `on_broadcast` is never called again and `user_data` is never touched again, so release
367/// `user_data` there. The broadcast handle is usable with [moq_consume_catalog] / [moq_consume_track]
368/// and must be freed separately with [moq_consume_close].
369///
370/// Returns a non-zero handle to the wait on success, or a negative code on (immediate) failure.
371///
372/// # Safety
373/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
374/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_broadcast` callback.
375#[unsafe(no_mangle)]
376pub unsafe extern "C" fn moq_origin_consume_announced(
377	origin: u32,
378	path: *const c_char,
379	path_len: usize,
380	on_broadcast: Option<extern "C" fn(user_data: *mut c_void, broadcast: i32)>,
381	user_data: *mut c_void,
382) -> i32 {
383	ffi::enter(move || {
384		let origin = ffi::parse_id(origin)?;
385		let path = unsafe { ffi::parse_str(path, path_len)? }.to_string();
386		let on_broadcast = unsafe { ffi::OnStatus::new(user_data, on_broadcast) };
387		State::lock().origin.consume_announced(origin, path, on_broadcast)
388	})
389}
390
391/// Abort a wait started by [moq_origin_consume_announced].
392///
393/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
394/// `user_data`. The [moq_origin_consume_announced] `on_broadcast` callback still fires once more
395/// with a terminal `0` (or a negative error), and that final callback is where `user_data` should
396/// be released. Any broadcast handle already delivered is unaffected and must still be freed with
397/// [moq_consume_close].
398#[unsafe(no_mangle)]
399pub extern "C" fn moq_origin_consume_announced_close(task: u32) -> i32 {
400	ffi::enter(move || {
401		let task = ffi::parse_id(task)?;
402		State::lock().origin.consume_announced_close(task)
403	})
404}
405
406/// Close an origin and clean up its resources.
407///
408/// Returns a zero on success, or a negative code on failure.
409#[unsafe(no_mangle)]
410pub extern "C" fn moq_origin_close(origin: u32) -> i32 {
411	ffi::enter(move || {
412		let origin = ffi::parse_id(origin)?;
413		State::lock().origin.close(origin)
414	})
415}
416
417/// Create a new broadcast for publishing media tracks.
418///
419/// Returns a non-zero handle to the broadcast on success, or a negative code on failure.
420#[unsafe(no_mangle)]
421pub extern "C" fn moq_publish_create() -> i32 {
422	ffi::enter(move || State::lock().publish.create())
423}
424
425/// Close a broadcast and clean up its resources.
426///
427/// Returns a zero on success, or a negative code on failure.
428#[unsafe(no_mangle)]
429pub extern "C" fn moq_publish_close(broadcast: u32) -> i32 {
430	ffi::enter(move || {
431		let broadcast = ffi::parse_id(broadcast)?;
432		State::lock().publish.close(broadcast)
433	})
434}
435
436/// Create a new media track for a broadcast
437///
438/// All frames in [moq_publish_media_frame] must be written in decode order.
439/// The `format` controls the encoding, both of `init` and frame payloads.
440///
441/// Returns a non-zero handle to the track on success, or a negative code on failure.
442///
443/// # Safety
444/// - The caller must ensure that format is a valid pointer to format_len bytes of data.
445/// - The caller must ensure that init is a valid pointer to init_size bytes of data.
446#[unsafe(no_mangle)]
447pub unsafe extern "C" fn moq_publish_media_ordered(
448	broadcast: u32,
449	format: *const c_char,
450	format_len: usize,
451	init: *const u8,
452	init_size: usize,
453) -> i32 {
454	ffi::enter(move || {
455		let broadcast = ffi::parse_id(broadcast)?;
456		let format = unsafe { ffi::parse_str(format, format_len)? };
457		let init = unsafe { ffi::parse_slice(init, init_size)? };
458
459		State::lock().publish.media_ordered(broadcast, format, init)
460	})
461}
462
463/// Remove a track from a broadcast.
464///
465/// Returns a zero on success, or a negative code on failure.
466#[unsafe(no_mangle)]
467pub extern "C" fn moq_publish_media_close(export: u32) -> i32 {
468	ffi::enter(move || {
469		let export = ffi::parse_id(export)?;
470		State::lock().publish.media_close(export)
471	})
472}
473
474/// Write data to a track.
475///
476/// The encoding of `data` depends on the track `format`.
477/// The timestamp is in microseconds.
478///
479/// Returns a zero on success, or a negative code on failure.
480///
481/// # Safety
482/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
483#[unsafe(no_mangle)]
484pub unsafe extern "C" fn moq_publish_media_frame(
485	media: u32,
486	payload: *const u8,
487	payload_size: usize,
488	timestamp_us: u64,
489) -> i32 {
490	ffi::enter(move || {
491		let media = ffi::parse_id(media)?;
492		let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
493		let timestamp = hang::container::Timestamp::from_micros(timestamp_us)?;
494		State::lock().publish.media_frame(media, payload, timestamp)
495	})
496}
497
498/// Add or replace a video rendition in a broadcast's catalog.
499///
500/// This is the producer counterpart to [moq_consume_video_config]: instead of
501/// reading a rendition out of a catalog, it writes one into the catalog of a
502/// broadcast created with [moq_publish_create]. The rendition is keyed by
503/// `config.name`; calling this again with the same name replaces it. The
504/// updated catalog is published to subscribers automatically.
505///
506/// The struct fields are read as inputs:
507/// - `name` / `codec` are required (NOT NULL terminated) string slices.
508/// - `description` may be NULL to omit it.
509/// - `coded_width` / `coded_height` may be NULL to omit them.
510///
511/// Returns a zero on success, or a negative code on failure.
512///
513/// # Safety
514/// - The caller must ensure that `config` points to a valid [moq_video_config].
515/// - The caller must ensure each non-NULL pointer inside `config` is valid for its length.
516#[unsafe(no_mangle)]
517pub unsafe extern "C" fn moq_publish_video_config(broadcast: u32, config: *const moq_video_config) -> i32 {
518	ffi::enter(move || {
519		let broadcast = ffi::parse_id(broadcast)?;
520		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
521
522		let name = unsafe { ffi::parse_str(config.name, config.name_len)? };
523		let codec = unsafe { ffi::parse_str(config.codec, config.codec_len)? };
524		let codec = hang::catalog::VideoCodec::from_str(codec).map_err(Error::Hang)?;
525
526		let mut video = hang::catalog::VideoConfig::new(codec);
527		if !config.description.is_null() {
528			let description = unsafe { ffi::parse_slice(config.description, config.description_len)? };
529			video.description = Some(bytes::Bytes::copy_from_slice(description));
530		}
531		video.coded_width = unsafe { config.coded_width.as_ref() }.copied();
532		video.coded_height = unsafe { config.coded_height.as_ref() }.copied();
533
534		State::lock().publish.video_config(broadcast, name, video)
535	})
536}
537
538/// Add or replace an audio rendition in a broadcast's catalog.
539///
540/// This is the producer counterpart to [moq_consume_audio_config]. The rendition
541/// is keyed by `config.name`; calling this again with the same name replaces it.
542/// The updated catalog is published to subscribers automatically.
543///
544/// The struct fields are read as inputs:
545/// - `name` / `codec` are required (NOT NULL terminated) string slices.
546/// - `sample_rate` / `channel_count` are required.
547/// - `description` may be NULL to omit it.
548///
549/// Returns a zero on success, or a negative code on failure.
550///
551/// # Safety
552/// - The caller must ensure that `config` points to a valid [moq_audio_config].
553/// - The caller must ensure each non-NULL pointer inside `config` is valid for its length.
554#[unsafe(no_mangle)]
555pub unsafe extern "C" fn moq_publish_audio_config(broadcast: u32, config: *const moq_audio_config) -> i32 {
556	ffi::enter(move || {
557		let broadcast = ffi::parse_id(broadcast)?;
558		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
559
560		let name = unsafe { ffi::parse_str(config.name, config.name_len)? };
561		let codec = unsafe { ffi::parse_str(config.codec, config.codec_len)? };
562		let codec = hang::catalog::AudioCodec::from_str(codec).map_err(Error::Hang)?;
563
564		let mut audio = hang::catalog::AudioConfig::new(codec, config.sample_rate, config.channel_count);
565		if !config.description.is_null() {
566			let description = unsafe { ffi::parse_slice(config.description, config.description_len)? };
567			audio.description = Some(bytes::Bytes::copy_from_slice(description));
568		}
569
570		State::lock().publish.audio_config(broadcast, name, audio)
571	})
572}
573
574/// Remove a video rendition from a broadcast's catalog by name.
575///
576/// This is a no-op if no rendition with that name exists. The updated catalog is
577/// published to subscribers automatically.
578///
579/// Returns a zero on success, or a negative code on failure.
580///
581/// # Safety
582/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
583#[unsafe(no_mangle)]
584pub unsafe extern "C" fn moq_publish_video_remove(broadcast: u32, name: *const c_char, name_len: usize) -> i32 {
585	ffi::enter(move || {
586		let broadcast = ffi::parse_id(broadcast)?;
587		let name = unsafe { ffi::parse_str(name, name_len)? };
588		State::lock().publish.video_remove(broadcast, name)
589	})
590}
591
592/// Remove an audio rendition from a broadcast's catalog by name.
593///
594/// This is a no-op if no rendition with that name exists. The updated catalog is
595/// published to subscribers automatically.
596///
597/// Returns a zero on success, or a negative code on failure.
598///
599/// # Safety
600/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
601#[unsafe(no_mangle)]
602pub unsafe extern "C" fn moq_publish_audio_remove(broadcast: u32, name: *const c_char, name_len: usize) -> i32 {
603	ffi::enter(move || {
604		let broadcast = ffi::parse_id(broadcast)?;
605		let name = unsafe { ffi::parse_str(name, name_len)? };
606		State::lock().publish.audio_remove(broadcast, name)
607	})
608}
609
610/// Set or replace an untyped application section in a broadcast's catalog.
611///
612/// `value` is a UTF-8 JSON document (object, array, string, ...) that lands as a top-level
613/// catalog key alongside `video`/`audio`, delivered to subscribers via [moq_consume_catalog_section].
614/// Use it to advertise a side-channel track the catalog doesn't model natively (a transcript,
615/// captions, ...). Calling this again with the same `name` replaces it; `name` must not be a
616/// reserved media section (`video`/`audio`). The updated catalog is published automatically.
617///
618/// Returns a zero on success, or a negative code on failure.
619///
620/// # Safety
621/// - The caller must ensure `name` / `value` are valid pointers to their respective lengths.
622#[unsafe(no_mangle)]
623pub unsafe extern "C" fn moq_publish_catalog_section(
624	broadcast: u32,
625	name: *const c_char,
626	name_len: usize,
627	value: *const c_char,
628	value_len: usize,
629) -> i32 {
630	ffi::enter(move || {
631		let broadcast = ffi::parse_id(broadcast)?;
632		let name = unsafe { ffi::parse_str(name, name_len)? };
633		let value = unsafe { ffi::parse_slice(value.cast::<u8>(), value_len)? };
634		let value = serde_json::from_slice(value)?;
635		State::lock().publish.catalog_section(broadcast, name, value)
636	})
637}
638
639/// Remove an untyped application section from a broadcast's catalog by name.
640///
641/// This is a no-op if no section with that name exists. The updated catalog is
642/// published to subscribers automatically.
643///
644/// Returns a zero on success, or a negative code on failure.
645///
646/// # Safety
647/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
648#[unsafe(no_mangle)]
649pub unsafe extern "C" fn moq_publish_catalog_section_remove(
650	broadcast: u32,
651	name: *const c_char,
652	name_len: usize,
653) -> i32 {
654	ffi::enter(move || {
655		let broadcast = ffi::parse_id(broadcast)?;
656		let name = unsafe { ffi::parse_str(name, name_len)? };
657		State::lock().publish.catalog_section_remove(broadcast, name)
658	})
659}
660
661/// Create a raw track on a broadcast for arbitrary byte payloads.
662///
663/// Unlike [moq_publish_media_ordered], this is the bare moq-net primitive: no
664/// codec, container, or catalog framing. Frames written to it are delivered
665/// as-is to subscribers using [moq_consume_track]. Use it for non-media tracks
666/// (control channels, JSON metadata, etc.), or pair it with
667/// [moq_publish_video_config] / [moq_publish_audio_config] to also describe the
668/// track in the catalog.
669///
670/// Returns a non-zero handle to the track on success, or a negative code on failure.
671///
672/// # Safety
673/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
674#[unsafe(no_mangle)]
675pub unsafe extern "C" fn moq_publish_track(broadcast: u32, name: *const c_char, name_len: usize) -> i32 {
676	ffi::enter(move || {
677		let broadcast = ffi::parse_id(broadcast)?;
678		let name = unsafe { ffi::parse_str(name, name_len)? };
679		State::lock().publish.track(broadcast, name)
680	})
681}
682
683/// Append a new group to a raw track, returning a group producer.
684///
685/// Groups are delivered independently and each may contain any number of frames
686/// written via [moq_publish_group_frame]. Sequence numbers auto-increment.
687///
688/// Returns a non-zero handle to the group on success, or a negative code on failure.
689#[unsafe(no_mangle)]
690pub extern "C" fn moq_publish_track_group(track: u32) -> i32 {
691	ffi::enter(move || {
692		let track = ffi::parse_id(track)?;
693		State::lock().publish.track_group(track)
694	})
695}
696
697/// Write a single-frame group to a raw track.
698///
699/// Convenience for the common one-frame-per-group pattern. Equivalent to
700/// appending a group, writing one frame, and finishing it.
701///
702/// Returns a zero on success, or a negative code on failure.
703///
704/// # Safety
705/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
706#[unsafe(no_mangle)]
707pub unsafe extern "C" fn moq_publish_track_frame(track: u32, payload: *const u8, payload_size: usize) -> i32 {
708	ffi::enter(move || {
709		let track = ffi::parse_id(track)?;
710		let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
711		State::lock().publish.track_frame(track, payload)
712	})
713}
714
715/// Finish a raw track. No more groups or frames can be written.
716///
717/// Returns a zero on success, or a negative code on failure.
718#[unsafe(no_mangle)]
719pub extern "C" fn moq_publish_track_close(track: u32) -> i32 {
720	ffi::enter(move || {
721		let track = ffi::parse_id(track)?;
722		State::lock().publish.track_finish(track)
723	})
724}
725
726/// Write a frame into a raw group created by [moq_publish_track_group].
727///
728/// Returns a zero on success, or a negative code on failure.
729///
730/// # Safety
731/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
732#[unsafe(no_mangle)]
733pub unsafe extern "C" fn moq_publish_group_frame(group: u32, payload: *const u8, payload_size: usize) -> i32 {
734	ffi::enter(move || {
735		let group = ffi::parse_id(group)?;
736		let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
737		State::lock().publish.group_frame(group, payload)
738	})
739}
740
741/// Finish a raw group. No more frames can be written.
742///
743/// Returns a zero on success, or a negative code on failure.
744#[unsafe(no_mangle)]
745pub extern "C" fn moq_publish_group_close(group: u32) -> i32 {
746	ffi::enter(move || {
747		let group = ffi::parse_id(group)?;
748		State::lock().publish.group_finish(group)
749	})
750}
751
752/// Create a JSON snapshot track (lossy latest-value) on a broadcast.
753///
754/// Values published via [moq_publish_json_update] reach subscribers as a single latest state; a
755/// late joiner only sees the newest. Advertise the track in the catalog with
756/// [moq_publish_catalog_section] if consumers should discover it.
757///
758/// Returns a non-zero handle to the JSON producer on success, or a negative code on failure.
759///
760/// # Safety
761/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
762#[unsafe(no_mangle)]
763pub unsafe extern "C" fn moq_publish_json(
764	broadcast: u32,
765	name: *const c_char,
766	name_len: usize,
767	config: *const moq_json_config,
768) -> i32 {
769	ffi::enter(move || {
770		let broadcast = ffi::parse_id(broadcast)?;
771		let name = unsafe { ffi::parse_str(name, name_len)? };
772		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
773		let mut producer = moq_json::snapshot::ProducerConfig::default();
774		producer.delta_ratio = config.delta_ratio;
775		producer.compression = config.compression;
776		State::lock().publish.json(broadcast, name, producer)
777	})
778}
779
780/// Publish a new value to a JSON snapshot track. `value` is a UTF-8 JSON document. A no-op if
781/// unchanged from the previous update.
782///
783/// Returns a zero on success, or a negative code on failure.
784///
785/// # Safety
786/// - The caller must ensure `value` is a valid pointer to `value_len` bytes.
787#[unsafe(no_mangle)]
788pub unsafe extern "C" fn moq_publish_json_update(json: u32, value: *const c_char, value_len: usize) -> i32 {
789	ffi::enter(move || {
790		let json = ffi::parse_id(json)?;
791		let value = unsafe { ffi::parse_slice(value.cast::<u8>(), value_len)? };
792		let value = serde_json::from_slice(value)?;
793		State::lock().publish.json_update(json, value)
794	})
795}
796
797/// Finish a JSON snapshot track. No more values can be published.
798///
799/// Returns a zero on success, or a negative code on failure.
800#[unsafe(no_mangle)]
801pub extern "C" fn moq_publish_json_close(json: u32) -> i32 {
802	ffi::enter(move || {
803		let json = ffi::parse_id(json)?;
804		State::lock().publish.json_close(json)
805	})
806}
807
808/// Create a JSON stream track (lossless append-log) on a broadcast.
809///
810/// Every record appended via [moq_publish_json_stream_append] is preserved and delivered in order.
811///
812/// Returns a non-zero handle to the JSON stream producer on success, or a negative code on failure.
813///
814/// # Safety
815/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
816#[unsafe(no_mangle)]
817pub unsafe extern "C" fn moq_publish_json_stream(
818	broadcast: u32,
819	name: *const c_char,
820	name_len: usize,
821	config: *const moq_json_stream_config,
822) -> i32 {
823	ffi::enter(move || {
824		let broadcast = ffi::parse_id(broadcast)?;
825		let name = unsafe { ffi::parse_str(name, name_len)? };
826		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
827		let producer = moq_json::stream::ProducerConfig::default().with_compression(config.compression);
828		State::lock().publish.json_stream(broadcast, name, producer)
829	})
830}
831
832/// Append one record to a JSON stream track. `value` is a UTF-8 JSON document.
833///
834/// Returns a zero on success, or a negative code on failure.
835///
836/// # Safety
837/// - The caller must ensure `value` is a valid pointer to `value_len` bytes.
838#[unsafe(no_mangle)]
839pub unsafe extern "C" fn moq_publish_json_stream_append(stream: u32, value: *const c_char, value_len: usize) -> i32 {
840	ffi::enter(move || {
841		let stream = ffi::parse_id(stream)?;
842		let value = unsafe { ffi::parse_slice(value.cast::<u8>(), value_len)? };
843		let value = serde_json::from_slice(value)?;
844		State::lock().publish.json_stream_append(stream, value)
845	})
846}
847
848/// Finish a JSON stream track. No more records can be appended.
849///
850/// Returns a zero on success, or a negative code on failure.
851#[unsafe(no_mangle)]
852pub extern "C" fn moq_publish_json_stream_close(stream: u32) -> i32 {
853	ffi::enter(move || {
854		let stream = ffi::parse_id(stream)?;
855		State::lock().publish.json_stream_close(stream)
856	})
857}
858
859/// Create a catalog consumer for a broadcast.
860///
861/// `on_catalog` is invoked with a positive catalog ID for each catalog update
862/// (usable to query video/audio track information), then exactly once more with
863/// a terminal code: `0` (closed cleanly) or a negative error. After the terminal
864/// (`<= 0`) callback, `on_catalog` is never called again and `user_data` is never
865/// touched again, so release `user_data` there. The terminal callback fires even
866/// after [moq_consume_catalog_close].
867///
868/// Returns a non-zero handle on success, or a negative code on failure.
869///
870/// # Safety
871/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_catalog` callback.
872#[unsafe(no_mangle)]
873pub unsafe extern "C" fn moq_consume_catalog(
874	broadcast: u32,
875	on_catalog: Option<extern "C" fn(user_data: *mut c_void, catalog: i32)>,
876	user_data: *mut c_void,
877) -> i32 {
878	ffi::enter(move || {
879		let broadcast = ffi::parse_id(broadcast)?;
880		let on_catalog = unsafe { ffi::OnStatus::new(user_data, on_catalog) };
881		State::lock().consume.catalog(broadcast, on_catalog)
882	})
883}
884
885/// Stop a catalog consumer's background subscription.
886///
887/// Returns immediately: zero on success, or a negative code if already closed.
888/// Does NOT free `user_data`; the [moq_consume_catalog] callback still fires once
889/// more with a terminal `0` (or a negative error), which is where `user_data`
890/// should be released. Catalog snapshots previously delivered via the callback
891/// remain valid until freed with [moq_consume_catalog_free].
892#[unsafe(no_mangle)]
893pub extern "C" fn moq_consume_catalog_close(catalog: u32) -> i32 {
894	ffi::enter(move || {
895		let catalog = ffi::parse_id(catalog)?;
896		State::lock().consume.catalog_close(catalog)
897	})
898}
899
900/// Free a catalog snapshot received via the [moq_consume_catalog] callback.
901///
902/// This releases the snapshot and invalidates any borrowed references (e.g. pointers
903/// returned by [moq_consume_video_config] or [moq_consume_audio_config]).
904///
905/// Returns a zero on success, or a negative code on failure.
906#[unsafe(no_mangle)]
907pub extern "C" fn moq_consume_catalog_free(catalog: u32) -> i32 {
908	ffi::enter(move || {
909		let catalog = ffi::parse_id(catalog)?;
910		State::lock().consume.catalog_free(catalog)
911	})
912}
913
914/// Query information about a video track in a catalog.
915///
916/// The destination is filled with the video track information.
917///
918/// Returns a zero on success, or a negative code on failure.
919///
920/// # Safety
921/// - The caller must ensure that `dst` is a valid pointer to a [moq_video_config] struct.
922/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
923#[unsafe(no_mangle)]
924pub unsafe extern "C" fn moq_consume_video_config(catalog: u32, index: u32, dst: *mut moq_video_config) -> i32 {
925	ffi::enter(move || {
926		let catalog = ffi::parse_id(catalog)?;
927		let index = index as usize;
928		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
929		State::lock().consume.video_config(catalog, index, dst)
930	})
931}
932
933/// Query information about an audio track in a catalog.
934///
935/// The destination is filled with the audio track information.
936///
937/// Returns a zero on success, or a negative code on failure.
938///
939/// # Safety
940/// - The caller must ensure that `dst` is a valid pointer to a [moq_audio_config] struct.
941/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
942#[unsafe(no_mangle)]
943pub unsafe extern "C" fn moq_consume_audio_config(catalog: u32, index: u32, dst: *mut moq_audio_config) -> i32 {
944	ffi::enter(move || {
945		let catalog = ffi::parse_id(catalog)?;
946		let index = index as usize;
947		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
948		State::lock().consume.audio_config(catalog, index, dst)
949	})
950}
951
952/// Query an untyped application section in a catalog by index.
953///
954/// Sections are the top-level catalog keys beyond `video`/`audio`, sorted by name. Iterate by
955/// incrementing `index` from zero until this returns the no-index error (-19); each call fills
956/// `dst` with the section's name and its value as a JSON document. Decode the JSON yourself.
957///
958/// Returns a zero on success, the no-index error when `index` is out of range, or another
959/// negative code on failure.
960///
961/// # Safety
962/// - The caller must ensure that `dst` is a valid pointer to a [moq_section] struct.
963/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
964#[unsafe(no_mangle)]
965pub unsafe extern "C" fn moq_consume_catalog_section(catalog: u32, index: u32, dst: *mut moq_section) -> i32 {
966	ffi::enter(move || {
967		let catalog = ffi::parse_id(catalog)?;
968		let index = index as usize;
969		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
970		State::lock().consume.catalog_section(catalog, index, dst)
971	})
972}
973
974/// Consume a video track from a broadcast, delivering frames in order.
975///
976/// - `max_latency_ms` controls the maximum amount of buffering allowed before skipping a GoP.
977/// - `on_frame` is called with a positive frame ID per frame, then exactly once
978///   more with a terminal code: `0` (closed cleanly) or a negative error. After
979///   the terminal (`<= 0`) callback, `on_frame` is never called again and
980///   `user_data` is never touched again, so release `user_data` there. The
981///   terminal callback fires even after [moq_consume_video_close].
982///
983/// Returns a non-zero handle to the track on success, or a negative code on failure.
984///
985/// # Safety
986/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
987#[unsafe(no_mangle)]
988pub unsafe extern "C" fn moq_consume_video_ordered(
989	catalog: u32,
990	index: u32,
991	max_latency_ms: u64,
992	on_frame: Option<extern "C" fn(user_data: *mut c_void, frame: i32)>,
993	user_data: *mut c_void,
994) -> i32 {
995	ffi::enter(move || {
996		let catalog = ffi::parse_id(catalog)?;
997		let index = index as usize;
998		let max_latency = std::time::Duration::from_millis(max_latency_ms);
999		let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame) };
1000		State::lock()
1001			.consume
1002			.video_ordered(catalog, index, max_latency, on_frame)
1003	})
1004}
1005
1006/// Stop a video track consumer's background task.
1007///
1008/// Returns immediately: zero on success, or a negative code if already closed.
1009/// Does NOT free `user_data`; the [moq_consume_video_ordered] `on_frame` callback
1010/// still fires once more with a terminal `0` (or a negative error), which is
1011/// where `user_data` should be released.
1012#[unsafe(no_mangle)]
1013pub extern "C" fn moq_consume_video_close(track: u32) -> i32 {
1014	ffi::enter(move || {
1015		let track = ffi::parse_id(track)?;
1016		State::lock().consume.track_close(track)
1017	})
1018}
1019
1020/// Consume an audio track from a broadcast, emitting the frames in order.
1021///
1022/// `on_frame` is called with a positive frame ID per frame, then exactly once
1023/// more with a terminal code: `0` (closed cleanly) or a negative error. After
1024/// the terminal (`<= 0`) callback, `on_frame` is never called again and
1025/// `user_data` is never touched again, so release `user_data` there. The
1026/// terminal callback fires even after [moq_consume_audio_close].
1027/// The `max_latency_ms` parameter controls how long to wait before skipping frames.
1028///
1029/// Returns a non-zero handle to the track on success, or a negative code on failure.
1030///
1031/// # Safety
1032/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
1033#[unsafe(no_mangle)]
1034pub unsafe extern "C" fn moq_consume_audio_ordered(
1035	catalog: u32,
1036	index: u32,
1037	max_latency_ms: u64,
1038	on_frame: Option<extern "C" fn(user_data: *mut c_void, frame: i32)>,
1039	user_data: *mut c_void,
1040) -> i32 {
1041	ffi::enter(move || {
1042		let catalog = ffi::parse_id(catalog)?;
1043		let index = index as usize;
1044		let max_latency = std::time::Duration::from_millis(max_latency_ms);
1045		let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame) };
1046		State::lock()
1047			.consume
1048			.audio_ordered(catalog, index, max_latency, on_frame)
1049	})
1050}
1051
1052/// Stop an audio track consumer's background task.
1053///
1054/// Returns immediately: zero on success, or a negative code if already closed.
1055/// Does NOT free `user_data`; the [moq_consume_audio_ordered] `on_frame` callback
1056/// still fires once more with a terminal `0` (or a negative error), which is
1057/// where `user_data` should be released.
1058#[unsafe(no_mangle)]
1059pub extern "C" fn moq_consume_audio_close(track: u32) -> i32 {
1060	ffi::enter(move || {
1061		let track = ffi::parse_id(track)?;
1062		State::lock().consume.track_close(track)
1063	})
1064}
1065
1066/// Get a chunk of a frame's payload.
1067///
1068/// Read the payload of a frame as a single contiguous slice.
1069///
1070/// Frames are not chunked; the entire payload is delivered through `dst.payload` /
1071/// `dst.payload_size` in one call. The pointer is valid until [`moq_consume_frame_close`]
1072/// is called for this frame.
1073///
1074/// Returns a zero on success, or a negative code on failure.
1075///
1076/// # Safety
1077/// - The caller must ensure that `dst` is a valid pointer to a [moq_frame] struct.
1078#[unsafe(no_mangle)]
1079pub unsafe extern "C" fn moq_consume_frame(frame: u32, dst: *mut moq_frame) -> i32 {
1080	ffi::enter(move || {
1081		let frame = ffi::parse_id(frame)?;
1082		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1083		State::lock().consume.frame(frame, dst)
1084	})
1085}
1086
1087/// Close a frame and clean up its resources.
1088///
1089/// Returns a zero on success, or a negative code on failure.
1090#[unsafe(no_mangle)]
1091pub extern "C" fn moq_consume_frame_close(frame: u32) -> i32 {
1092	ffi::enter(move || {
1093		let frame = ffi::parse_id(frame)?;
1094		State::lock().consume.frame_close(frame)
1095	})
1096}
1097
1098/// Close a broadcast consumer and clean up its resources.
1099///
1100/// Returns a zero on success, or a negative code on failure.
1101#[unsafe(no_mangle)]
1102pub extern "C" fn moq_consume_close(consume: u32) -> i32 {
1103	ffi::enter(move || {
1104		let consume = ffi::parse_id(consume)?;
1105		State::lock().consume.close(consume)
1106	})
1107}
1108
1109/// Subscribe to a raw track by name, delivering each frame's payload as-is.
1110///
1111/// This is the counterpart to [moq_publish_track]: no catalog lookup or
1112/// container parsing. `on_frame` is called with a positive raw frame ID for each
1113/// frame in arrival order, then exactly once more with a terminal code: `0`
1114/// (closed cleanly) or a negative error. After the terminal (`<= 0`) callback,
1115/// `on_frame` is never called again and `user_data` is never touched again, so
1116/// release `user_data` there. The terminal callback fires even after
1117/// [moq_consume_track_close]. Read each frame with [moq_consume_track_frame] and
1118/// release it with [moq_consume_track_frame_close].
1119///
1120/// Returns a non-zero handle to the track on success, or a negative code on failure.
1121///
1122/// # Safety
1123/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
1124/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
1125#[unsafe(no_mangle)]
1126pub unsafe extern "C" fn moq_consume_track(
1127	broadcast: u32,
1128	name: *const c_char,
1129	name_len: usize,
1130	on_frame: Option<extern "C" fn(user_data: *mut c_void, frame: i32)>,
1131	user_data: *mut c_void,
1132) -> i32 {
1133	ffi::enter(move || {
1134		let broadcast = ffi::parse_id(broadcast)?;
1135		let name = unsafe { ffi::parse_str(name, name_len)? };
1136		let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame) };
1137		State::lock().consume.raw_track(broadcast, name, on_frame)
1138	})
1139}
1140
1141/// Read a raw frame's payload delivered via the [moq_consume_track] callback.
1142///
1143/// Fills `dst.payload` / `dst.payload_size`; the pointer is valid until the
1144/// frame is released with [moq_consume_frame_close]. `dst.timestamp_us` and
1145/// `dst.keyframe` are reported as 0 / false (not meaningful for raw tracks).
1146///
1147/// Returns a zero on success, or a negative code on failure.
1148///
1149/// # Safety
1150/// - The caller must ensure that `dst` is a valid pointer to a [moq_frame] struct.
1151#[unsafe(no_mangle)]
1152pub unsafe extern "C" fn moq_consume_track_frame(frame: u32, dst: *mut moq_frame) -> i32 {
1153	ffi::enter(move || {
1154		let frame = ffi::parse_id(frame)?;
1155		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1156		State::lock().consume.raw_frame(frame, dst)
1157	})
1158}
1159
1160/// Close a raw frame and clean up its resources.
1161///
1162/// Returns a zero on success, or a negative code on failure.
1163#[unsafe(no_mangle)]
1164pub extern "C" fn moq_consume_track_frame_close(frame: u32) -> i32 {
1165	ffi::enter(move || {
1166		let frame = ffi::parse_id(frame)?;
1167		State::lock().consume.raw_frame_close(frame)
1168	})
1169}
1170
1171/// Stop a raw track consumer's background task.
1172///
1173/// Returns immediately: zero on success, or a negative code if already closed.
1174/// Does NOT free `user_data`; the [moq_consume_track] `on_frame` callback still
1175/// fires once more with a terminal `0` (or a negative error), which is where
1176/// `user_data` should be released. Frames already delivered via the callback
1177/// remain valid until released with [moq_consume_track_frame_close].
1178#[unsafe(no_mangle)]
1179pub extern "C" fn moq_consume_track_close(track: u32) -> i32 {
1180	ffi::enter(move || {
1181		let track = ffi::parse_id(track)?;
1182		State::lock().consume.raw_track_close(track)
1183	})
1184}
1185
1186/// Subscribe to a JSON snapshot track (lossy latest-value) by name.
1187///
1188/// `on_value` is called with a positive value ID for each new latest value; a consumer that
1189/// falls behind collapses the backlog and only sees the newest. It is called exactly once more
1190/// with a terminal `0` (track ended / closed) or a negative error, after which `user_data` is
1191/// never touched again, so release it there. Read each value with [moq_consume_json_value] and
1192/// release it with [moq_consume_json_value_close]. Pass the same compression the producer used.
1193///
1194/// Returns a non-zero handle to the task on success, or a negative code on failure.
1195///
1196/// # Safety
1197/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
1198/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_value` callback.
1199#[unsafe(no_mangle)]
1200pub unsafe extern "C" fn moq_consume_json(
1201	broadcast: u32,
1202	name: *const c_char,
1203	name_len: usize,
1204	config: *const moq_json_config,
1205	on_value: Option<extern "C" fn(user_data: *mut c_void, value: i32)>,
1206	user_data: *mut c_void,
1207) -> i32 {
1208	ffi::enter(move || {
1209		let broadcast = ffi::parse_id(broadcast)?;
1210		let name = unsafe { ffi::parse_str(name, name_len)? };
1211		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1212		let mut consumer = moq_json::snapshot::ConsumerConfig::default();
1213		consumer.compression = config.compression;
1214		let on_value = unsafe { ffi::OnStatus::new(user_data, on_value) };
1215		State::lock().consume.json(broadcast, name, consumer, on_value)
1216	})
1217}
1218
1219/// Subscribe to a JSON stream track (lossless append-log) by name.
1220///
1221/// `on_value` is called with a positive value ID for each record, in order, then once more with
1222/// a terminal `0` or negative error where `user_data` should be released. Read each value with
1223/// [moq_consume_json_value] and release it with [moq_consume_json_value_close].
1224///
1225/// Returns a non-zero handle to the task on success, or a negative code on failure.
1226///
1227/// # Safety
1228/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
1229/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_value` callback.
1230#[unsafe(no_mangle)]
1231pub unsafe extern "C" fn moq_consume_json_stream(
1232	broadcast: u32,
1233	name: *const c_char,
1234	name_len: usize,
1235	config: *const moq_json_stream_config,
1236	on_value: Option<extern "C" fn(user_data: *mut c_void, value: i32)>,
1237	user_data: *mut c_void,
1238) -> i32 {
1239	ffi::enter(move || {
1240		let broadcast = ffi::parse_id(broadcast)?;
1241		let name = unsafe { ffi::parse_str(name, name_len)? };
1242		let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1243		let consumer = moq_json::stream::ConsumerConfig::default().with_compression(config.compression);
1244		let on_value = unsafe { ffi::OnStatus::new(user_data, on_value) };
1245		State::lock().consume.json_stream(broadcast, name, consumer, on_value)
1246	})
1247}
1248
1249/// Read a JSON value delivered via a [moq_consume_json] or [moq_consume_json_stream] callback.
1250///
1251/// Fills `dst.json` / `dst.json_len`; the pointer is valid until the value is released with
1252/// [moq_consume_json_value_close].
1253///
1254/// Returns a zero on success, or a negative code on failure.
1255///
1256/// # Safety
1257/// - The caller must ensure `dst` is a valid pointer to a [moq_json_value] struct.
1258#[unsafe(no_mangle)]
1259pub unsafe extern "C" fn moq_consume_json_value(value: u32, dst: *mut moq_json_value) -> i32 {
1260	ffi::enter(move || {
1261		let value = ffi::parse_id(value)?;
1262		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1263		State::lock().consume.json_value(value, dst)
1264	})
1265}
1266
1267/// Release a JSON value delivered via a consumer callback.
1268///
1269/// Returns a zero on success, or a negative code on failure.
1270#[unsafe(no_mangle)]
1271pub extern "C" fn moq_consume_json_value_close(value: u32) -> i32 {
1272	ffi::enter(move || {
1273		let value = ffi::parse_id(value)?;
1274		State::lock().consume.json_value_close(value)
1275	})
1276}
1277
1278/// Stop a JSON consumer's background task (snapshot or stream).
1279///
1280/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
1281/// `user_data`; the `on_value` callback still fires once more with a terminal `0` (or a negative
1282/// error), which is where `user_data` should be released. Values already delivered remain valid
1283/// until released with [moq_consume_json_value_close].
1284#[unsafe(no_mangle)]
1285pub extern "C" fn moq_consume_json_close(task: u32) -> i32 {
1286	ffi::enter(move || {
1287		let task = ffi::parse_id(task)?;
1288		State::lock().consume.json_close(task)
1289	})
1290}