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