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