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