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