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