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