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/// Options for a JSON snapshot track (lossy latest-value mode).
57///
58/// The same config is passed to a producer and its consumers, but the consumer reads only
59/// `compression`; `delta_ratio` is producer-only.
60#[repr(C)]
61#[allow(non_camel_case_types)]
62pub struct moq_json_snapshot_config {
63 /// How aggressively the producer emits deltas instead of full snapshots. `0` disables deltas
64 /// (one snapshot per group); a positive value allows roughly that many snapshots' worth of
65 /// deltas before rolling. Ignored by the consumer.
66 pub delta_ratio: u32,
67
68 /// DEFLATE-compress each group. Must match on the producer and consumer.
69 pub compression: bool,
70}
71
72/// Options for a JSON stream track (lossless append-log mode).
73#[repr(C)]
74#[allow(non_camel_case_types)]
75pub struct moq_json_stream_config {
76 /// DEFLATE-compress the group. Must match on the producer and consumer.
77 pub compression: bool,
78}
79
80/// A JSON value delivered by a consumer callback.
81#[repr(C)]
82#[allow(non_camel_case_types)]
83pub struct moq_json_value {
84 /// The JSON document as UTF-8, NOT NULL terminated.
85 pub json: *const c_char,
86 pub json_len: usize,
87}
88
89/// Information about a frame of media.
90#[repr(C)]
91#[allow(non_camel_case_types)]
92pub struct moq_frame {
93 /// The payload of the frame, or NULL/0 if the stream has ended
94 pub payload: *const u8,
95 pub payload_size: usize,
96
97 /// The presentation timestamp of the frame in microseconds
98 pub timestamp_us: u64,
99
100 /// Whether the frame is a keyframe, aka the start of a new group.
101 pub keyframe: bool,
102}
103
104/// A best-effort raw track datagram delivered via [moq_consume_datagrams].
105#[repr(C)]
106#[allow(non_camel_case_types)]
107pub struct moq_datagram {
108 /// The payload of the datagram, or NULL/0 if the track has ended.
109 pub payload: *const u8,
110 pub payload_size: usize,
111
112 /// The presentation timestamp of the datagram in microseconds.
113 pub timestamp_us: u64,
114
115 /// Per-track sequence number, drawn from the same namespace as groups.
116 pub sequence: u64,
117}
118
119/// Publisher-side raw track properties.
120///
121/// A null [moq_publish_track] `info` pointer uses the moq-net defaults.
122/// A zero-initialized struct also uses those defaults, except `priority` where
123/// zero is the default itself.
124#[repr(C)]
125#[allow(non_camel_case_types)]
126pub struct moq_track_info {
127 /// Priority, used to break ties between subscriptions of equal subscriber priority.
128 pub priority: u8,
129
130 /// Whether groups are prioritized in sequence order.
131 /// Groups may always arrive out-of-order (or not at all) over the network.
132 pub ordered: bool,
133
134 /// Maximum age of a non-latest group before the publisher evicts it, in milliseconds.
135 /// The publisher-side half of `moq_subscription.latency_max_ms`.
136 pub latency_max_ms: u64,
137 /// Whether `latency_max_ms` should override the default.
138 pub latency_max_valid: bool,
139
140 /// Per-frame timescale in ticks per second.
141 pub timescale: u64,
142 /// Whether `timescale` should override the default microsecond timescale,
143 /// which matches the `timestamp_us` units used everywhere else in this ABI.
144 pub timescale_valid: bool,
145}
146
147impl TryFrom<&moq_track_info> for moq_net::track::Info {
148 type Error = Error;
149
150 fn try_from(info: &moq_track_info) -> Result<Self, Self::Error> {
151 // Raw tracks default to a microsecond timescale, matching the C ABI's
152 // timestamp_us units. An explicit timescale below overrides it.
153 let mut out = moq_net::track::Info::default()
154 .with_timescale(moq_net::Timescale::MICRO)
155 .with_priority(info.priority)
156 .with_ordered(info.ordered);
157 if info.latency_max_valid {
158 out = out.with_latency_max(std::time::Duration::from_millis(info.latency_max_ms));
159 }
160 if info.timescale_valid {
161 out = out.with_timescale(moq_net::Timescale::new(info.timescale)?);
162 }
163 Ok(out)
164 }
165}
166
167/// Subscriber-side raw track delivery preferences.
168///
169/// A null [moq_consume_track] or [moq_consume_track_update] `subscription`
170/// pointer uses the moq-net defaults.
171#[repr(C)]
172#[allow(non_camel_case_types)]
173pub struct moq_subscription {
174 /// Delivery priority. Higher values preempt lower ones under contention.
175 pub priority: u8,
176
177 /// Whether groups are prioritized in sequence order.
178 /// Groups may always arrive out-of-order (or not at all) over the network.
179 pub ordered: bool,
180
181 /// Maximum age of a non-latest group before it is skipped, in milliseconds.
182 /// Zero skips immediately. Enforced by the publisher's cache and by any local buffering.
183 pub latency_max_ms: u64,
184
185 /// First group to deliver.
186 pub group_start: u64,
187 /// Whether `group_start` is present. When false, delivery starts at the latest group.
188 pub group_start_valid: bool,
189
190 /// Last group to deliver, inclusive.
191 pub group_end: u64,
192 /// Whether `group_end` is present. When false, there is no end cap.
193 pub group_end_valid: bool,
194}
195
196impl From<&moq_subscription> for moq_net::track::Subscription {
197 fn from(subscription: &moq_subscription) -> Self {
198 let mut out = moq_net::track::Subscription::default()
199 .with_priority(subscription.priority)
200 .with_ordered(subscription.ordered)
201 .with_latency_max(std::time::Duration::from_millis(subscription.latency_max_ms));
202 if subscription.group_start_valid {
203 out = out.with_group_start(subscription.group_start);
204 }
205 if subscription.group_end_valid {
206 out = out.with_group_end(subscription.group_end);
207 }
208 out
209 }
210}
211
212/// A borrowed UTF-8 string slice, NOT NULL terminated.
213///
214/// Used to hand a C caller a JSON document that lives inside libmoq's storage.
215/// The pointer borrows that storage and is only valid until the owning resource
216/// is freed (see the function that fills it for the exact lifetime).
217#[repr(C)]
218#[allow(non_camel_case_types)]
219pub struct moq_string {
220 /// Pointer to `len` bytes of UTF-8, NOT NULL terminated.
221 pub data: *const c_char,
222 pub len: usize,
223}
224
225/// One untyped application catalog section: a name and its JSON value.
226///
227/// Both `name` and `json` are UTF-8, NOT NULL terminated, and borrow the catalog
228/// snapshot's storage. They stay valid until the snapshot is freed with
229/// [moq_consume_catalog_free]. `json` is the section's value serialized as JSON
230/// (parse it yourself); a top-level catalog key beyond `video`/`audio`.
231#[repr(C)]
232#[allow(non_camel_case_types)]
233pub struct moq_section {
234 /// The section name, NOT NULL terminated.
235 pub name: *const c_char,
236 pub name_len: usize,
237
238 /// The section value as a JSON document, NOT NULL terminated.
239 pub json: *const c_char,
240 pub json_len: usize,
241}
242
243/// Information about a broadcast announced by an origin.
244#[repr(C)]
245#[allow(non_camel_case_types)]
246pub struct moq_announced {
247 /// The path of the broadcast, NOT NULL terminated
248 pub path: *const c_char,
249 pub path_len: usize,
250
251 /// Whether the broadcast is active or has ended
252 /// This MUST toggle between true and false over the lifetime of the broadcast
253 pub active: bool,
254}
255
256/// A snapshot of connection statistics, filled in by [moq_session_stats].
257///
258/// Each metric has a `*_valid` flag: when `false`, the matching value is meaningless because
259/// the transport backend doesn't report it (a `false` flag is NOT the same as a zero value).
260/// Native QUIC reports every metric; the browser WebTransport reports few or none. Initialize
261/// the struct to zero before the call; [moq_session_stats] overwrites every field.
262#[repr(C)]
263#[allow(non_camel_case_types)]
264pub struct moq_connection_stats {
265 /// Smoothed round-trip time, in microseconds.
266 pub rtt_us: u64,
267 pub rtt_valid: bool,
268
269 /// Estimated send bandwidth from the congestion controller, in bits per second.
270 pub send_rate_bps: u64,
271 pub send_rate_valid: bool,
272
273 /// Estimated receive bandwidth from MoQ PROBE, in bits per second.
274 pub recv_rate_bps: u64,
275 pub recv_rate_valid: bool,
276
277 /// Total bytes sent, including retransmissions and overhead.
278 pub bytes_sent: u64,
279 pub bytes_sent_valid: bool,
280
281 /// Total bytes received, including duplicates and overhead.
282 pub bytes_received: u64,
283 pub bytes_received_valid: bool,
284
285 /// Total bytes lost (detected via retransmission or acknowledgement).
286 pub bytes_lost: u64,
287 pub bytes_lost_valid: bool,
288
289 /// Total datagrams sent.
290 pub packets_sent: u64,
291 pub packets_sent_valid: bool,
292
293 /// Total datagrams received.
294 pub packets_received: u64,
295 pub packets_received_valid: bool,
296
297 /// Total datagrams detected as lost.
298 pub packets_lost: u64,
299 pub packets_lost_valid: bool,
300}
301
302impl From<&moq_net::ConnectionStats> for moq_connection_stats {
303 fn from(stats: &moq_net::ConnectionStats) -> Self {
304 // An Option<u64> becomes a (value, valid) pair; absent metrics report 0/false.
305 fn split(value: Option<u64>) -> (u64, bool) {
306 (value.unwrap_or(0), value.is_some())
307 }
308
309 let (rtt_us, rtt_valid) = split(stats.rtt.map(|d| d.as_micros() as u64));
310 let (send_rate_bps, send_rate_valid) = split(stats.estimated_send_rate);
311 let (recv_rate_bps, recv_rate_valid) = split(stats.estimated_recv_rate);
312 let (bytes_sent, bytes_sent_valid) = split(stats.bytes_sent);
313 let (bytes_received, bytes_received_valid) = split(stats.bytes_received);
314 let (bytes_lost, bytes_lost_valid) = split(stats.bytes_lost);
315 let (packets_sent, packets_sent_valid) = split(stats.packets_sent);
316 let (packets_received, packets_received_valid) = split(stats.packets_received);
317 let (packets_lost, packets_lost_valid) = split(stats.packets_lost);
318
319 Self {
320 rtt_us,
321 rtt_valid,
322 send_rate_bps,
323 send_rate_valid,
324 recv_rate_bps,
325 recv_rate_valid,
326 bytes_sent,
327 bytes_sent_valid,
328 bytes_received,
329 bytes_received_valid,
330 bytes_lost,
331 bytes_lost_valid,
332 packets_sent,
333 packets_sent_valid,
334 packets_received,
335 packets_received_valid,
336 packets_lost,
337 packets_lost_valid,
338 }
339 }
340}
341
342/// Initialize the library with a log level.
343///
344/// This should be called before any other functions.
345/// The log_level is a string: "error", "warn", "info", "debug", "trace"
346///
347/// Returns a zero on success, or a negative code on failure.
348///
349/// # Safety
350/// - The caller must ensure that level is a valid pointer to level_len bytes of data.
351#[unsafe(no_mangle)]
352pub unsafe extern "C" fn moq_log_level(level: *const c_char, level_len: usize) -> i32 {
353 ffi::enter(move || {
354 match unsafe { ffi::parse_str(level, level_len)? } {
355 "" => moq_native::Log::default(),
356 level => moq_native::Log::new(Level::from_str(level)?),
357 }
358 .init()?;
359
360 Ok(())
361 })
362}
363
364/// Human-readable reason for the most recent failed call on the calling thread.
365///
366/// libmoq functions return only a negative code; this exposes the matching message
367/// (including detail the code can't carry, e.g. which URL failed to parse or why a
368/// decode failed). The string is only meaningful after a call returned a negative
369/// code; check the code first.
370///
371/// Returns a NUL-terminated, UTF-8 pointer valid until the next libmoq call **on the
372/// same thread**, or NULL if no error has been recorded on this thread. Copy it if you
373/// need it to outlive the next call. Errors delivered through status callbacks carry
374/// their code directly; read this from inside the callback to get their reason.
375#[unsafe(no_mangle)]
376pub extern "C" fn moq_error() -> *const c_char {
377 ffi::last_error_ptr()
378}
379
380/// Start establishing a connection to a MoQ server.
381///
382/// Takes origin handles, which are used for publishing and consuming broadcasts respectively.
383/// - Any broadcasts in `origin_publish` will be announced to the server.
384/// - Any broadcasts announced by the server will be available in `origin_consume`.
385/// - If an origin handle is 0, that functionality is completely disabled.
386///
387/// This may be called multiple times to connect to different servers.
388/// Origins can be shared across sessions, useful for fanout or relaying.
389///
390/// Returns a non-zero handle to the session on success, or a negative code on (immediate) failure.
391/// You should call [moq_session_close], even on error, to free up resources.
392///
393/// The session reconnects automatically with exponential backoff if the connection drops.
394/// Published broadcasts are re-announced and consumers re-subscribed on each reconnect,
395/// since the origins outlive the underlying connection.
396///
397/// `on_status` reports the session lifecycle through its status code:
398/// - `> 0` on every (re)connect, carrying the connection epoch (`1` = first connect,
399/// `2` = first reconnect, and so on), so a reconnect is distinguishable from the
400/// initial connect. May fire repeatedly. Transient disconnects are not reported.
401/// - `0` when the session is closed cleanly via [moq_session_close] (terminal).
402/// - a negative error code if reconnection permanently gives up, e.g. the backoff
403/// timeout is exceeded (terminal).
404///
405/// After a terminal (`<= 0`) status, `on_status` is never called again and `user_data`
406/// is never touched again, so that final callback is the point to release `user_data`.
407/// The terminal `0` fires even after [moq_session_close], so do not free `user_data` on
408/// the close call itself.
409///
410/// # Safety
411/// - The caller must ensure that url is a valid pointer to url_len bytes of data.
412/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_status` callback.
413#[unsafe(no_mangle)]
414pub unsafe extern "C" fn moq_session_connect(
415 url: *const c_char,
416 url_len: usize,
417 origin_publish: u32,
418 origin_consume: u32,
419 on_status: Option<extern "C" fn(user_data: *mut c_void, code: i32)>,
420 user_data: *mut c_void,
421) -> i32 {
422 ffi::enter(move || {
423 let url = ffi::parse_url(url, url_len)?;
424
425 let mut state = State::lock();
426 let publish = ffi::parse_id_optional(origin_publish)?
427 .map(|id| state.origin.get(id))
428 .transpose()?
429 .cloned();
430 let consume = ffi::parse_id_optional(origin_consume)?
431 .map(|id| state.origin.get(id))
432 .transpose()?
433 .cloned();
434
435 let on_status = unsafe { ffi::OnStatus::new(user_data, on_status) };
436 state.session.connect(url, publish, consume, on_status)
437 })
438}
439
440/// Request that a session shut down.
441///
442/// Returns immediately: zero on success, or a negative code if the session is
443/// unknown or already closing. Does NOT free `user_data`. The
444/// [moq_session_connect] `on_status` callback still fires once more with a
445/// terminal `0` (or a negative error), and that final callback is where
446/// `user_data` should be released. Safe to call from any thread, including from
447/// within `on_status`.
448#[unsafe(no_mangle)]
449pub extern "C" fn moq_session_close(session: u32) -> i32 {
450 ffi::enter(move || {
451 let session = ffi::parse_id(session)?;
452 State::lock().session.close(session)
453 })
454}
455
456/// Snapshot the current connection statistics for a session.
457///
458/// Fills `dst` with a point-in-time view of the underlying QUIC/WebTransport connection
459/// (RTT, bandwidth estimates, byte/packet counters). Each metric carries a `*_valid` flag
460/// since availability depends on the transport backend; see [moq_connection_stats].
461///
462/// Returns zero on success, or a negative code on failure: the session handle is unknown, or
463/// the session is currently reconnecting and has no live connection (in which case `dst` is
464/// left untouched). Safe to call repeatedly to poll stats over the life of the session.
465///
466/// # Safety
467/// - The caller must ensure that `dst` is a valid pointer to a [moq_connection_stats] struct.
468#[unsafe(no_mangle)]
469pub unsafe extern "C" fn moq_session_stats(session: u32, dst: *mut moq_connection_stats) -> i32 {
470 ffi::enter(move || {
471 let session = ffi::parse_id(session)?;
472 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
473 let stats = State::lock().session.stats(session)?;
474 *dst = moq_connection_stats::from(&stats);
475 Ok(())
476 })
477}
478
479/// Create an origin for publishing broadcasts.
480///
481/// Origins contain any number of broadcasts addressed by path.
482/// The same broadcast can be published to multiple origins under different paths.
483///
484/// [moq_origin_announced] can be used to discover broadcasts published to this origin.
485/// This is extremely useful for discovering what is available on the server to [moq_origin_request].
486///
487/// Returns a non-zero handle to the origin on success.
488#[unsafe(no_mangle)]
489pub extern "C" fn moq_origin_create() -> i32 {
490 ffi::enter(move || State::lock().origin.create())
491}
492
493/// Create a broadcast at `path` on an origin, for publishing media tracks.
494///
495/// The broadcast starts live: the origin announces the path so consumers can discover it,
496/// becoming visible shortly after this returns. Fill it with the `moq_publish_*` functions.
497/// Toggle discoverability with [moq_publish_set_announce]; [moq_publish_finish] unpublishes
498/// immediately.
499///
500/// Returns a non-zero broadcast handle on success, or a negative code on failure.
501///
502/// # Safety
503/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
504#[unsafe(no_mangle)]
505pub unsafe extern "C" fn moq_origin_publish(origin: u32, path: *const c_char, path_len: usize) -> i32 {
506 ffi::enter(move || {
507 let origin = ffi::parse_id(origin)?;
508 let path = unsafe { ffi::parse_str(path, path_len)? };
509
510 let mut state = State::lock();
511 let broadcast = state.origin.publish(origin, path)?;
512 state.publish.create(broadcast)
513 })
514}
515
516/// Learn about all broadcasts published to an origin.
517///
518/// `on_announce` is invoked with a positive announced ID for each broadcast,
519/// then exactly once more with a terminal code: `0` (stopped cleanly) or a
520/// negative error. After the terminal (`<= 0`) callback, `on_announce` is never
521/// called again and `user_data` is never touched again, so release `user_data`
522/// there. The terminal callback fires even after [moq_origin_announced_close].
523///
524/// - [moq_origin_announced_info] is used to query information about the broadcast.
525/// - [moq_origin_announced_free] releases each delivered announced ID once read.
526/// - [moq_origin_announced_close] is used to stop receiving announcements.
527///
528/// Returns a non-zero handle on success, or a negative code on failure.
529///
530/// # Safety
531/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_announce` callback.
532#[unsafe(no_mangle)]
533pub unsafe extern "C" fn moq_origin_announced(
534 origin: u32,
535 on_announce: Option<extern "C" fn(user_data: *mut c_void, announced: i32)>,
536 user_data: *mut c_void,
537) -> i32 {
538 ffi::enter(move || {
539 let origin = ffi::parse_id(origin)?;
540 let on_announce = unsafe { ffi::OnStatus::new(user_data, on_announce) };
541 State::lock().origin.announced(origin, on_announce)
542 })
543}
544
545/// Query information about a broadcast discovered by [moq_origin_announced].
546///
547/// The destination is filled with the broadcast information. The `path` pointer borrows
548/// the announcement's storage: copy it out before calling [moq_origin_announced_free], which
549/// invalidates it.
550///
551/// Returns a zero on success, or a negative code on failure.
552///
553/// # Safety
554/// - The caller must ensure that `dst` is a valid pointer to a [moq_announced] struct.
555#[unsafe(no_mangle)]
556pub unsafe extern "C" fn moq_origin_announced_info(announced: u32, dst: *mut moq_announced) -> i32 {
557 ffi::enter(move || {
558 let announced = ffi::parse_id(announced)?;
559 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
560 State::lock().origin.announced_info(announced, dst)
561 })
562}
563
564/// Free a single announcement delivered to a [moq_origin_announced] `on_announce` callback.
565///
566/// Each announce / unannounce event hands the callback a distinct announcement handle (read
567/// with [moq_origin_announced_info]); release it here once done to avoid leaking one per event
568/// over the life of the listener. This is per-announcement and distinct from
569/// [moq_origin_announced_close], which stops the listener itself. After freeing, any `path`
570/// pointer obtained from [moq_origin_announced_info] for this handle is dangling.
571///
572/// Returns zero on success, or a negative code if the handle is unknown.
573#[unsafe(no_mangle)]
574pub extern "C" fn moq_origin_announced_free(announced: u32) -> i32 {
575 ffi::enter(move || {
576 let announced = ffi::parse_id(announced)?;
577 State::lock().origin.announced_free(announced)
578 })
579}
580
581/// Stop receiving announcements for broadcasts published to an origin.
582///
583/// Returns immediately: zero on success, or a negative code if already closed.
584/// Does NOT free `user_data`. The [moq_origin_announced] `on_announce` callback
585/// still fires once more with a terminal `0` (or a negative error), and that
586/// final callback is where `user_data` should be released.
587#[unsafe(no_mangle)]
588pub extern "C" fn moq_origin_announced_close(announced: u32) -> i32 {
589 ffi::enter(move || {
590 let announced = ffi::parse_id(announced)?;
591 State::lock().origin.announced_close(announced)
592 })
593}
594
595/// Consume a broadcast from an origin by path, waiting until it is announced.
596///
597/// Resolves against future announcements: it waits for the announcement to arrive (e.g. over the
598/// network) and then delivers the broadcast handle via `on_broadcast`. Use it right after
599/// [moq_session_connect] to avoid racing announcement gossip. To resolve against only what is
600/// announced now (plus any dynamic fallback), use [moq_origin_request] instead.
601///
602/// `on_broadcast` is invoked with a positive broadcast handle once announced, then exactly once
603/// more with a terminal code: `0` (the wait finished, including after
604/// [moq_origin_consume_announced_close]) or a negative error. After the terminal (`<= 0`) callback,
605/// `on_broadcast` is never called again and `user_data` is never touched again, so release
606/// `user_data` there. The broadcast handle is usable with [moq_consume_catalog] / [moq_consume_track]
607/// and must be freed separately with [moq_consume_close].
608///
609/// Returns a non-zero handle to the wait on success, or a negative code on (immediate) failure.
610///
611/// # Safety
612/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
613/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_broadcast` callback.
614#[unsafe(no_mangle)]
615pub unsafe extern "C" fn moq_origin_consume_announced(
616 origin: u32,
617 path: *const c_char,
618 path_len: usize,
619 on_broadcast: Option<extern "C" fn(user_data: *mut c_void, broadcast: i32)>,
620 user_data: *mut c_void,
621) -> i32 {
622 ffi::enter(move || {
623 let origin = ffi::parse_id(origin)?;
624 let path = unsafe { ffi::parse_str(path, path_len)? }.to_string();
625 let on_broadcast = unsafe { ffi::OnStatus::new(user_data, on_broadcast) };
626 State::lock().origin.consume_announced(origin, path, on_broadcast)
627 })
628}
629
630/// Abort a wait started by [moq_origin_consume_announced].
631///
632/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
633/// `user_data`. The [moq_origin_consume_announced] `on_broadcast` callback still fires once more
634/// with a terminal `0` (or a negative error), and that final callback is where `user_data` should
635/// be released. Any broadcast handle already delivered is unaffected and must still be freed with
636/// [moq_consume_close].
637#[unsafe(no_mangle)]
638pub extern "C" fn moq_origin_consume_announced_close(task: u32) -> i32 {
639 ffi::enter(move || {
640 let task = ffi::parse_id(task)?;
641 State::lock().origin.consume_announced_close(task)
642 })
643}
644
645/// Request a broadcast from an origin by path, resolving as soon as it can be served.
646///
647/// Resolves against what is announced *now* plus any dynamic fallback, where
648/// [moq_origin_consume_announced] waits indefinitely for a future announcement: it returns an
649/// already-announced broadcast at once, otherwise falls back to a dynamic handler on the origin
650/// (if any), and fails when neither can serve the path. It does NOT wait for a later
651/// announcement.
652///
653/// `on_broadcast` is invoked with a positive broadcast handle once served, then exactly once more
654/// with a terminal code: `0` (finished, including after [moq_origin_request_close]) or a negative
655/// error. After the terminal (`<= 0`) callback, `user_data` is never touched again, so release it
656/// there. The broadcast handle is usable with [moq_consume_catalog] / [moq_consume_track] and must
657/// be freed separately with [moq_consume_close].
658///
659/// Returns a non-zero handle to the request on success, or a negative code on (immediate) failure.
660///
661/// # Safety
662/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
663/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_broadcast` callback.
664#[unsafe(no_mangle)]
665pub unsafe extern "C" fn moq_origin_request(
666 origin: u32,
667 path: *const c_char,
668 path_len: usize,
669 on_broadcast: Option<extern "C" fn(user_data: *mut c_void, broadcast: i32)>,
670 user_data: *mut c_void,
671) -> i32 {
672 ffi::enter(move || {
673 let origin = ffi::parse_id(origin)?;
674 let path = unsafe { ffi::parse_str(path, path_len)? }.to_string();
675 let on_broadcast = unsafe { ffi::OnStatus::new(user_data, on_broadcast) };
676 State::lock().origin.request(origin, path, on_broadcast)
677 })
678}
679
680/// Abort a request started by [moq_origin_request].
681///
682/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
683/// `user_data`; the [moq_origin_request] `on_broadcast` callback fires once more with a terminal
684/// code, which is where `user_data` should be released. Any broadcast handle already delivered is
685/// unaffected and must still be freed with [moq_consume_close].
686#[unsafe(no_mangle)]
687pub extern "C" fn moq_origin_request_close(task: u32) -> i32 {
688 ffi::enter(move || {
689 let task = ffi::parse_id(task)?;
690 State::lock().origin.consume_announced_close(task)
691 })
692}
693
694/// Close an origin and clean up its resources.
695///
696/// Returns a zero on success, or a negative code on failure.
697#[unsafe(no_mangle)]
698pub extern "C" fn moq_origin_close(origin: u32) -> i32 {
699 ffi::enter(move || {
700 let origin = ffi::parse_id(origin)?;
701 State::lock().origin.close(origin)
702 })
703}
704
705/// Set whether a broadcast created by [moq_origin_publish] is live: announced by its origin.
706///
707/// A non-live broadcast stays reachable by exact path for subscribes and fetches; it just is
708/// not announced. This is how a publisher goes on and off the air without tearing down the
709/// broadcast.
710///
711/// Returns a zero on success, or a negative code on failure.
712#[unsafe(no_mangle)]
713pub extern "C" fn moq_publish_set_announce(broadcast: u32, announce: bool) -> i32 {
714 ffi::enter(move || {
715 let broadcast = ffi::parse_id(broadcast)?;
716 State::lock().publish.set_announce(broadcast, announce)
717 })
718}
719
720/// Finish a broadcast and release it, ending its catalog cleanly.
721///
722/// Subscribers see a normal end of stream rather than an error, and the origin unpublishes
723/// the path immediately.
724///
725/// Returns a zero on success, or a negative code on failure.
726#[unsafe(no_mangle)]
727pub extern "C" fn moq_publish_finish(broadcast: u32) -> i32 {
728 ffi::enter(move || {
729 let broadcast = ffi::parse_id(broadcast)?;
730 State::lock().publish.finish(broadcast)
731 })
732}
733
734/// Create a new media track for a broadcast
735///
736/// All frames in [moq_publish_media_frame] must be written in decode order.
737/// The `format` controls the encoding, both of `init` and frame payloads.
738///
739/// Returns a non-zero handle to the track on success, or a negative code on failure.
740///
741/// # Safety
742/// - The caller must ensure that format is a valid pointer to format_len bytes of data.
743/// - The caller must ensure that init is a valid pointer to init_size bytes of data.
744#[unsafe(no_mangle)]
745pub unsafe extern "C" fn moq_publish_media(
746 broadcast: u32,
747 format: *const c_char,
748 format_len: usize,
749 init: *const u8,
750 init_size: usize,
751) -> i32 {
752 ffi::enter(move || {
753 let broadcast = ffi::parse_id(broadcast)?;
754 let format = unsafe { ffi::parse_str(format, format_len)? };
755 let init = unsafe { ffi::parse_slice(init, init_size)? };
756
757 State::lock().publish.media(broadcast, format, init)
758 })
759}
760
761/// Finish a media track, flushing any buffered frames. No more frames can be written.
762///
763/// Returns a zero on success, or a negative code on failure.
764#[unsafe(no_mangle)]
765pub extern "C" fn moq_publish_media_finish(export: u32) -> i32 {
766 ffi::enter(move || {
767 let export = ffi::parse_id(export)?;
768 State::lock().publish.media_finish(export)
769 })
770}
771
772/// Write data to a track.
773///
774/// The encoding of `data` depends on the track `format`.
775/// The timestamp is in microseconds.
776///
777/// Returns a zero on success, or a negative code on failure.
778///
779/// # Safety
780/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
781#[unsafe(no_mangle)]
782pub unsafe extern "C" fn moq_publish_media_frame(
783 media: u32,
784 payload: *const u8,
785 payload_size: usize,
786 timestamp_us: u64,
787) -> i32 {
788 ffi::enter(move || {
789 let media = ffi::parse_id(media)?;
790 let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
791 let timestamp = hang::container::Timestamp::from_micros(timestamp_us)?;
792 State::lock().publish.media_frame(media, payload, timestamp)
793 })
794}
795
796/// Add or replace a video rendition in a broadcast's catalog.
797///
798/// This is the producer counterpart to [moq_consume_video_config]: instead of
799/// reading a rendition out of a catalog, it writes one into the catalog of a
800/// broadcast created with [moq_origin_publish]. The rendition is keyed by
801/// `config.name`; calling this again with the same name replaces it. The
802/// updated catalog is published to subscribers automatically.
803///
804/// The struct fields are read as inputs:
805/// - `name` / `codec` are required (NOT NULL terminated) string slices.
806/// - `description` may be NULL to omit it.
807/// - `coded_width` / `coded_height` may be NULL to omit them.
808///
809/// Returns a zero on success, or a negative code on failure.
810///
811/// # Safety
812/// - The caller must ensure that `config` points to a valid [moq_video_config].
813/// - The caller must ensure each non-NULL pointer inside `config` is valid for its length.
814#[unsafe(no_mangle)]
815pub unsafe extern "C" fn moq_publish_video_config(broadcast: u32, config: *const moq_video_config) -> i32 {
816 ffi::enter(move || {
817 let broadcast = ffi::parse_id(broadcast)?;
818 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
819
820 let name = unsafe { ffi::parse_str(config.name, config.name_len)? };
821 let codec = unsafe { ffi::parse_str(config.codec, config.codec_len)? };
822 let codec = hang::catalog::VideoCodec::from_str(codec).map_err(Error::Hang)?;
823
824 let mut video = hang::catalog::VideoConfig::new(codec);
825 if !config.description.is_null() {
826 let description = unsafe { ffi::parse_slice(config.description, config.description_len)? };
827 video.description = Some(bytes::Bytes::copy_from_slice(description));
828 }
829 video.coded_width = unsafe { config.coded_width.as_ref() }.copied();
830 video.coded_height = unsafe { config.coded_height.as_ref() }.copied();
831
832 State::lock().publish.video_config(broadcast, name, video)
833 })
834}
835
836/// Add or replace an audio rendition in a broadcast's catalog.
837///
838/// This is the producer counterpart to [moq_consume_audio_config]. The rendition
839/// is keyed by `config.name`; calling this again with the same name replaces it.
840/// The updated catalog is published to subscribers automatically.
841///
842/// The struct fields are read as inputs:
843/// - `name` / `codec` are required (NOT NULL terminated) string slices.
844/// - `sample_rate` / `channel_count` are required.
845/// - `description` may be NULL to omit it.
846///
847/// Returns a zero on success, or a negative code on failure.
848///
849/// # Safety
850/// - The caller must ensure that `config` points to a valid [moq_audio_config].
851/// - The caller must ensure each non-NULL pointer inside `config` is valid for its length.
852#[unsafe(no_mangle)]
853pub unsafe extern "C" fn moq_publish_audio_config(broadcast: u32, config: *const moq_audio_config) -> i32 {
854 ffi::enter(move || {
855 let broadcast = ffi::parse_id(broadcast)?;
856 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
857
858 let name = unsafe { ffi::parse_str(config.name, config.name_len)? };
859 let codec = unsafe { ffi::parse_str(config.codec, config.codec_len)? };
860 let codec = hang::catalog::AudioCodec::from_str(codec).map_err(Error::Hang)?;
861
862 let mut audio = hang::catalog::AudioConfig::new(codec, config.sample_rate, config.channel_count);
863 if !config.description.is_null() {
864 let description = unsafe { ffi::parse_slice(config.description, config.description_len)? };
865 audio.description = Some(bytes::Bytes::copy_from_slice(description));
866 }
867
868 State::lock().publish.audio_config(broadcast, name, audio)
869 })
870}
871
872/// Remove a video rendition from a broadcast's catalog by name.
873///
874/// This is a no-op if no rendition with that name exists. The updated catalog is
875/// published to subscribers automatically.
876///
877/// Returns a zero on success, or a negative code on failure.
878///
879/// # Safety
880/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
881#[unsafe(no_mangle)]
882pub unsafe extern "C" fn moq_publish_video_remove(broadcast: u32, name: *const c_char, name_len: usize) -> i32 {
883 ffi::enter(move || {
884 let broadcast = ffi::parse_id(broadcast)?;
885 let name = unsafe { ffi::parse_str(name, name_len)? };
886 State::lock().publish.video_remove(broadcast, name)
887 })
888}
889
890/// Remove an audio rendition from a broadcast's catalog by name.
891///
892/// This is a no-op if no rendition with that name exists. The updated catalog is
893/// published to subscribers automatically.
894///
895/// Returns a zero 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#[unsafe(no_mangle)]
900pub unsafe extern "C" fn moq_publish_audio_remove(broadcast: u32, name: *const c_char, name_len: usize) -> i32 {
901 ffi::enter(move || {
902 let broadcast = ffi::parse_id(broadcast)?;
903 let name = unsafe { ffi::parse_str(name, name_len)? };
904 State::lock().publish.audio_remove(broadcast, name)
905 })
906}
907
908/// Set (or replace) a top-level application catalog section by name.
909///
910/// This is the producer counterpart to [moq_consume_catalog_section] /
911/// [moq_consume_catalog_section_at]: it writes an arbitrary top-level JSON key into the
912/// catalog of a broadcast created with [moq_origin_publish], beyond the
913/// `video`/`audio` keys owned by the media pipeline. Calling it again with the
914/// same name replaces the section. The updated catalog is published to
915/// subscribers automatically.
916///
917/// `json` is a JSON document (object, array, string, ...) as `json_len` bytes of
918/// UTF-8. Returns a zero on success, or a negative code on failure: invalid JSON
919/// yields a Json error (-37); a reserved `name` (`video`/`audio`) yields a mux error.
920///
921/// # Safety
922/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
923/// - The caller must ensure that json is a valid pointer to json_len bytes of data.
924#[unsafe(no_mangle)]
925pub unsafe extern "C" fn moq_publish_catalog_section(
926 broadcast: u32,
927 name: *const c_char,
928 name_len: usize,
929 json: *const c_char,
930 json_len: usize,
931) -> i32 {
932 ffi::enter(move || {
933 let broadcast = ffi::parse_id(broadcast)?;
934 let name = unsafe { ffi::parse_str(name, name_len)? };
935 let json = unsafe { ffi::parse_str(json, json_len)? };
936 let value: serde_json::Value = serde_json::from_str(json)?;
937 State::lock().publish.catalog_section_set(broadcast, name, value)
938 })
939}
940
941/// Remove a top-level application catalog section by name.
942///
943/// This is a no-op if no section with that name exists. The updated catalog is
944/// published to subscribers automatically.
945///
946/// Returns a zero on success, or a negative code on failure.
947///
948/// # Safety
949/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
950#[unsafe(no_mangle)]
951pub unsafe extern "C" fn moq_publish_catalog_section_remove(
952 broadcast: u32,
953 name: *const c_char,
954 name_len: usize,
955) -> i32 {
956 ffi::enter(move || {
957 let broadcast = ffi::parse_id(broadcast)?;
958 let name = unsafe { ffi::parse_str(name, name_len)? };
959 State::lock().publish.catalog_section_remove(broadcast, name)
960 })
961}
962
963/// Create a raw track on a broadcast for arbitrary byte payloads.
964///
965/// Unlike [moq_publish_media], this is the bare moq-net primitive: no
966/// codec, container, or catalog framing. Frames written to it are delivered
967/// as-is to subscribers using [moq_consume_track]. Use it for non-media tracks
968/// (control channels, JSON metadata, etc.), or pair it with
969/// [moq_publish_video_config] / [moq_publish_audio_config] to also describe the
970/// track in the catalog. Pass NULL for `info` to use moq-net defaults.
971///
972/// Returns a non-zero handle to the track on success, or a negative code on failure.
973///
974/// # Safety
975/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
976/// - The caller must ensure that info is either NULL or a valid pointer to a [moq_track_info] struct.
977#[unsafe(no_mangle)]
978pub unsafe extern "C" fn moq_publish_track(
979 broadcast: u32,
980 name: *const c_char,
981 name_len: usize,
982 info: *const moq_track_info,
983) -> i32 {
984 ffi::enter(move || {
985 let broadcast = ffi::parse_id(broadcast)?;
986 let name = unsafe { ffi::parse_str(name, name_len)? };
987 // Default raw tracks to a microsecond timescale even when no info is given.
988 let info = match unsafe { info.as_ref() } {
989 Some(info) => moq_net::track::Info::try_from(info)?,
990 None => moq_net::track::Info::default().with_timescale(moq_net::Timescale::MICRO),
991 };
992 State::lock().publish.track(broadcast, name, Some(info))
993 })
994}
995
996/// Append a new group to a raw track, returning a group producer.
997///
998/// Groups are delivered independently and each may contain any number of frames
999/// written via [moq_publish_group_frame]. Sequence numbers auto-increment.
1000///
1001/// Returns a non-zero handle to the group on success, or a negative code on failure.
1002#[unsafe(no_mangle)]
1003pub extern "C" fn moq_publish_track_group(track: u32) -> i32 {
1004 ffi::enter(move || {
1005 let track = ffi::parse_id(track)?;
1006 State::lock().publish.track_group(track)
1007 })
1008}
1009
1010/// Create a raw group with an explicit sequence number.
1011///
1012/// Returns a non-zero group handle on success, or a negative code on failure.
1013#[unsafe(no_mangle)]
1014pub extern "C" fn moq_publish_track_group_at(track: u32, sequence: u64) -> i32 {
1015 ffi::enter(move || {
1016 let track = ffi::parse_id(track)?;
1017 State::lock().publish.track_group_at(track, sequence)
1018 })
1019}
1020
1021/// Write a single-frame group to a raw track with a timestamp.
1022///
1023/// Convenience for the common one-frame-per-group pattern. Equivalent to
1024/// appending a group, writing one frame, and finishing it.
1025/// The timestamp is in microseconds.
1026///
1027/// Returns a zero on success, or a negative code on failure.
1028///
1029/// # Safety
1030/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
1031#[unsafe(no_mangle)]
1032pub unsafe extern "C" fn moq_publish_track_frame(
1033 track: u32,
1034 payload: *const u8,
1035 payload_size: usize,
1036 timestamp_us: u64,
1037) -> i32 {
1038 ffi::enter(move || {
1039 let track = ffi::parse_id(track)?;
1040 let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
1041 let timestamp = moq_net::Timestamp::from_micros(timestamp_us)?;
1042 State::lock().publish.track_frame(track, timestamp, payload)
1043 })
1044}
1045
1046/// Send a best-effort datagram on a raw track created by [moq_publish_track].
1047///
1048/// Takes `payload` then `timestamp_us`, matching [moq_publish_track_frame]. The payload must
1049/// be at most 1200 bytes. On success the datagram's per-track sequence number (shared with the
1050/// group namespace) is written to `out_sequence` when it is non-NULL. Datagrams are
1051/// delivered only on transports and wire versions with a datagram channel; there is no
1052/// group fallback.
1053///
1054/// Returns a zero on success, or a negative code on failure.
1055///
1056/// # Safety
1057/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
1058/// - `out_sequence` must be NULL or a valid pointer to a `uint64_t`.
1059#[unsafe(no_mangle)]
1060pub unsafe extern "C" fn moq_publish_track_datagram(
1061 track: u32,
1062 payload: *const u8,
1063 payload_size: usize,
1064 timestamp_us: u64,
1065 out_sequence: *mut u64,
1066) -> i32 {
1067 ffi::enter(move || {
1068 let track = ffi::parse_id(track)?;
1069 let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
1070 let sequence = State::lock().publish.track_datagram(track, timestamp_us, payload)?;
1071 if let Some(out) = unsafe { out_sequence.as_mut() } {
1072 *out = sequence;
1073 }
1074 Ok(())
1075 })
1076}
1077
1078/// Finish a raw track. No more groups or frames can be written.
1079///
1080/// Returns a zero on success, or a negative code on failure.
1081#[unsafe(no_mangle)]
1082pub extern "C" fn moq_publish_track_finish(track: u32) -> i32 {
1083 ffi::enter(move || {
1084 let track = ffi::parse_id(track)?;
1085 State::lock().publish.track_finish(track)
1086 })
1087}
1088
1089/// Declare a raw track's exclusive final group sequence.
1090///
1091/// Groups below `final_sequence` may still be created. Groups at or above it
1092/// are rejected. The track remains open for groups below the boundary. Call
1093/// [moq_publish_track_finish] after producing the remaining groups.
1094#[unsafe(no_mangle)]
1095pub extern "C" fn moq_publish_track_finish_at(track: u32, final_sequence: u64) -> i32 {
1096 ffi::enter(move || {
1097 let track = ffi::parse_id(track)?;
1098 State::lock().publish.track_finish_at(track, final_sequence)
1099 })
1100}
1101
1102/// Abort a raw track with an application error code.
1103#[unsafe(no_mangle)]
1104pub extern "C" fn moq_publish_track_abort(track: u32, error_code: u16) -> i32 {
1105 ffi::enter(move || {
1106 let track = ffi::parse_id(track)?;
1107 State::lock().publish.track_abort(track, error_code)
1108 })
1109}
1110
1111/// Write a frame into a raw group created by [moq_publish_track_group].
1112///
1113/// The timestamp is in microseconds.
1114///
1115/// Returns a zero on success, or a negative code on failure.
1116///
1117/// # Safety
1118/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
1119#[unsafe(no_mangle)]
1120pub unsafe extern "C" fn moq_publish_group_frame(
1121 group: u32,
1122 payload: *const u8,
1123 payload_size: usize,
1124 timestamp_us: u64,
1125) -> i32 {
1126 ffi::enter(move || {
1127 let group = ffi::parse_id(group)?;
1128 let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
1129 let timestamp = moq_net::Timestamp::from_micros(timestamp_us)?;
1130 State::lock().publish.group_frame(group, timestamp, payload)
1131 })
1132}
1133
1134/// Finish a raw group. No more frames can be written.
1135///
1136/// Returns a zero on success, or a negative code on failure.
1137#[unsafe(no_mangle)]
1138pub extern "C" fn moq_publish_group_finish(group: u32) -> i32 {
1139 ffi::enter(move || {
1140 let group = ffi::parse_id(group)?;
1141 State::lock().publish.group_finish(group)
1142 })
1143}
1144
1145/// Abort a raw group with an application error code.
1146#[unsafe(no_mangle)]
1147pub extern "C" fn moq_publish_group_abort(group: u32, error_code: u16) -> i32 {
1148 ffi::enter(move || {
1149 let group = ffi::parse_id(group)?;
1150 State::lock().publish.group_abort(group, error_code)
1151 })
1152}
1153
1154/// Create a JSON snapshot track (lossy latest-value) on a broadcast.
1155///
1156/// Values published via [moq_publish_json_snapshot_update] reach subscribers as a single latest
1157/// state; a late joiner only sees the newest. Advertise the track in the catalog with
1158/// [moq_publish_catalog_section] if consumers should discover it.
1159///
1160/// Returns a non-zero handle to the JSON producer on success, or a negative code on failure.
1161///
1162/// # Safety
1163/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
1164#[unsafe(no_mangle)]
1165pub unsafe extern "C" fn moq_publish_json_snapshot(
1166 broadcast: u32,
1167 name: *const c_char,
1168 name_len: usize,
1169 config: *const moq_json_snapshot_config,
1170) -> i32 {
1171 ffi::enter(move || {
1172 let broadcast = ffi::parse_id(broadcast)?;
1173 let name = unsafe { ffi::parse_str(name, name_len)? };
1174 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1175 let mut producer = moq_json::snapshot::ProducerConfig::default();
1176 producer.delta_ratio = config.delta_ratio;
1177 producer.compression = config.compression;
1178 State::lock().publish.json_snapshot(broadcast, name, producer)
1179 })
1180}
1181
1182/// Publish a new value to a JSON snapshot track. `value` is a UTF-8 JSON document. A no-op if
1183/// unchanged from the previous update.
1184///
1185/// Returns a zero on success, or a negative code on failure.
1186///
1187/// # Safety
1188/// - The caller must ensure `value` is a valid pointer to `value_len` bytes.
1189#[unsafe(no_mangle)]
1190pub unsafe extern "C" fn moq_publish_json_snapshot_update(json: u32, value: *const c_char, value_len: usize) -> i32 {
1191 ffi::enter(move || {
1192 let json = ffi::parse_id(json)?;
1193 let value = unsafe { ffi::parse_slice(value.cast::<u8>(), value_len)? };
1194 let value = serde_json::from_slice(value)?;
1195 State::lock().publish.json_snapshot_update(json, value)
1196 })
1197}
1198
1199/// Finish a JSON snapshot track. No more values can be published.
1200///
1201/// Returns a zero on success, or a negative code on failure.
1202#[unsafe(no_mangle)]
1203pub extern "C" fn moq_publish_json_snapshot_finish(json: u32) -> i32 {
1204 ffi::enter(move || {
1205 let json = ffi::parse_id(json)?;
1206 State::lock().publish.json_snapshot_finish(json)
1207 })
1208}
1209
1210/// Create a JSON stream track (lossless append-log) on a broadcast.
1211///
1212/// Every record appended via [moq_publish_json_stream_append] is preserved and delivered in order.
1213///
1214/// Returns a non-zero handle to the JSON stream producer on success, or a negative code on failure.
1215///
1216/// # Safety
1217/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
1218#[unsafe(no_mangle)]
1219pub unsafe extern "C" fn moq_publish_json_stream(
1220 broadcast: u32,
1221 name: *const c_char,
1222 name_len: usize,
1223 config: *const moq_json_stream_config,
1224) -> i32 {
1225 ffi::enter(move || {
1226 let broadcast = ffi::parse_id(broadcast)?;
1227 let name = unsafe { ffi::parse_str(name, name_len)? };
1228 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1229 let producer = moq_json::stream::ProducerConfig::default().with_compression(config.compression);
1230 State::lock().publish.json_stream(broadcast, name, producer)
1231 })
1232}
1233
1234/// Append one record to a JSON stream track. `value` is a UTF-8 JSON document.
1235///
1236/// Returns a zero on success, or a negative code on failure.
1237///
1238/// # Safety
1239/// - The caller must ensure `value` is a valid pointer to `value_len` bytes.
1240#[unsafe(no_mangle)]
1241pub unsafe extern "C" fn moq_publish_json_stream_append(stream: u32, value: *const c_char, value_len: usize) -> i32 {
1242 ffi::enter(move || {
1243 let stream = ffi::parse_id(stream)?;
1244 let value = unsafe { ffi::parse_slice(value.cast::<u8>(), value_len)? };
1245 let value = serde_json::from_slice(value)?;
1246 State::lock().publish.json_stream_append(stream, value)
1247 })
1248}
1249
1250/// Finish a JSON stream track. No more records can be appended.
1251///
1252/// Returns a zero on success, or a negative code on failure.
1253#[unsafe(no_mangle)]
1254pub extern "C" fn moq_publish_json_stream_finish(stream: u32) -> i32 {
1255 ffi::enter(move || {
1256 let stream = ffi::parse_id(stream)?;
1257 State::lock().publish.json_stream_finish(stream)
1258 })
1259}
1260
1261/// Create a catalog consumer for a broadcast.
1262///
1263/// `on_catalog` is invoked with a positive catalog ID for each catalog update
1264/// (usable to query video/audio track information), then exactly once more with
1265/// a terminal code: `0` (closed cleanly) or a negative error. After the terminal
1266/// (`<= 0`) callback, `on_catalog` is never called again and `user_data` is never
1267/// touched again, so release `user_data` there. The terminal callback fires even
1268/// after [moq_consume_catalog_close].
1269///
1270/// Returns a non-zero handle on success, or a negative code on failure.
1271///
1272/// # Safety
1273/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_catalog` callback.
1274#[unsafe(no_mangle)]
1275pub unsafe extern "C" fn moq_consume_catalog(
1276 broadcast: u32,
1277 on_catalog: Option<extern "C" fn(user_data: *mut c_void, catalog: i32)>,
1278 user_data: *mut c_void,
1279) -> i32 {
1280 ffi::enter(move || {
1281 let broadcast = ffi::parse_id(broadcast)?;
1282 let on_catalog = unsafe { ffi::OnStatus::new(user_data, on_catalog) };
1283 State::lock().consume.catalog(broadcast, on_catalog)
1284 })
1285}
1286
1287/// Stop a catalog consumer's background subscription.
1288///
1289/// Returns immediately: zero on success, or a negative code if already closed.
1290/// Does NOT free `user_data`; the [moq_consume_catalog] callback still fires once
1291/// more with a terminal `0` (or a negative error), which is where `user_data`
1292/// should be released. Catalog snapshots previously delivered via the callback
1293/// remain valid until freed with [moq_consume_catalog_free].
1294#[unsafe(no_mangle)]
1295pub extern "C" fn moq_consume_catalog_close(catalog: u32) -> i32 {
1296 ffi::enter(move || {
1297 let catalog = ffi::parse_id(catalog)?;
1298 State::lock().consume.catalog_close(catalog)
1299 })
1300}
1301
1302/// Free a catalog snapshot received via the [moq_consume_catalog] callback.
1303///
1304/// This releases the snapshot and invalidates any borrowed references (e.g. pointers
1305/// returned by [moq_consume_video_config] or [moq_consume_audio_config]).
1306///
1307/// Returns a zero on success, or a negative code on failure.
1308#[unsafe(no_mangle)]
1309pub extern "C" fn moq_consume_catalog_free(catalog: u32) -> i32 {
1310 ffi::enter(move || {
1311 let catalog = ffi::parse_id(catalog)?;
1312 State::lock().consume.catalog_free(catalog)
1313 })
1314}
1315
1316/// Query information about a video track in a catalog.
1317///
1318/// The destination is filled with the video track information.
1319///
1320/// Returns a zero on success, or a negative code on failure.
1321///
1322/// # Safety
1323/// - The caller must ensure that `dst` is a valid pointer to a [moq_video_config] struct.
1324/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
1325#[unsafe(no_mangle)]
1326pub unsafe extern "C" fn moq_consume_video_config(catalog: u32, index: u32, dst: *mut moq_video_config) -> i32 {
1327 ffi::enter(move || {
1328 let catalog = ffi::parse_id(catalog)?;
1329 let index = index as usize;
1330 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1331 State::lock().consume.video_config(catalog, index, dst)
1332 })
1333}
1334
1335/// Query information about an audio track in a catalog.
1336///
1337/// The destination is filled with the audio track information.
1338///
1339/// Returns a zero on success, or a negative code on failure.
1340///
1341/// # Safety
1342/// - The caller must ensure that `dst` is a valid pointer to a [moq_audio_config] struct.
1343/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
1344#[unsafe(no_mangle)]
1345pub unsafe extern "C" fn moq_consume_audio_config(catalog: u32, index: u32, dst: *mut moq_audio_config) -> i32 {
1346 ffi::enter(move || {
1347 let catalog = ffi::parse_id(catalog)?;
1348 let index = index as usize;
1349 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1350 State::lock().consume.audio_config(catalog, index, dst)
1351 })
1352}
1353
1354/// Number of untyped application catalog sections in a catalog snapshot.
1355///
1356/// These are the top-level catalog keys beyond `video`/`audio`, carried through
1357/// verbatim. Iterate them by index with [moq_consume_catalog_section_at], or look one up
1358/// directly by name with [moq_consume_catalog_section].
1359///
1360/// Returns the count (>= 0) on success, or a negative code on failure.
1361#[unsafe(no_mangle)]
1362pub extern "C" fn moq_consume_catalog_section_count(catalog: u32) -> i32 {
1363 ffi::enter(move || {
1364 let catalog = ffi::parse_id(catalog)?;
1365 State::lock().consume.catalog_section_count(catalog)
1366 })
1367}
1368
1369/// Query an application catalog section by index, keyed by name.
1370///
1371/// Fills `dst` with the section's name and JSON value at `index`, in the range
1372/// `[0, moq_consume_catalog_section_count)`. Both pointers borrow the snapshot's storage
1373/// and stay valid until it is freed with [moq_consume_catalog_free].
1374///
1375/// Returns a zero on success, or a negative code on failure (e.g. `index` out of
1376/// range).
1377///
1378/// # Safety
1379/// - The caller must ensure that `dst` is a valid pointer to a [moq_section] struct.
1380/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
1381#[unsafe(no_mangle)]
1382pub unsafe extern "C" fn moq_consume_catalog_section_at(catalog: u32, index: u32, dst: *mut moq_section) -> i32 {
1383 ffi::enter(move || {
1384 let catalog = ffi::parse_id(catalog)?;
1385 let index = index as usize;
1386 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1387 State::lock().consume.catalog_section_at(catalog, index, dst)
1388 })
1389}
1390
1391/// Look up an application catalog section by name.
1392///
1393/// Fills `dst` with the section's JSON value (the document to parse yourself).
1394/// The pointer borrows the snapshot's storage and stays valid until it is freed
1395/// with [moq_consume_catalog_free].
1396///
1397/// Returns a zero on success, or a negative code on failure: no section with that
1398/// name yields a not-found error.
1399///
1400/// # Safety
1401/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
1402/// - The caller must ensure that `dst` is a valid pointer to a [moq_string] struct.
1403/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
1404#[unsafe(no_mangle)]
1405pub unsafe extern "C" fn moq_consume_catalog_section(
1406 catalog: u32,
1407 name: *const c_char,
1408 name_len: usize,
1409 dst: *mut moq_string,
1410) -> i32 {
1411 ffi::enter(move || {
1412 let catalog = ffi::parse_id(catalog)?;
1413 let name = unsafe { ffi::parse_str(name, name_len)? };
1414 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1415 State::lock().consume.catalog_section_get(catalog, name, dst)
1416 })
1417}
1418
1419/// Consume a video track from a broadcast, delivering frames in order.
1420///
1421/// - `max_latency_ms` controls the maximum amount of buffering allowed before skipping a GoP.
1422/// - `on_frame` is called with a positive frame ID per frame, then exactly once
1423/// more with a terminal code: `0` (closed cleanly) or a negative error. After
1424/// the terminal (`<= 0`) callback, `on_frame` is never called again and
1425/// `user_data` is never touched again, so release `user_data` there. The
1426/// terminal callback fires even after [moq_consume_video_close].
1427///
1428/// Returns a non-zero handle to the track on success, or a negative code on failure.
1429///
1430/// # Safety
1431/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
1432#[unsafe(no_mangle)]
1433pub unsafe extern "C" fn moq_consume_video(
1434 catalog: u32,
1435 index: u32,
1436 max_latency_ms: u64,
1437 on_frame: Option<extern "C" fn(user_data: *mut c_void, frame: i32)>,
1438 user_data: *mut c_void,
1439) -> i32 {
1440 ffi::enter(move || {
1441 let catalog = ffi::parse_id(catalog)?;
1442 let index = index as usize;
1443 let max_latency = std::time::Duration::from_millis(max_latency_ms);
1444 let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame) };
1445 State::lock().consume.video(catalog, index, max_latency, on_frame)
1446 })
1447}
1448
1449/// Stop a video track consumer's background task.
1450///
1451/// Returns immediately: zero on success, or a negative code if already closed.
1452/// Does NOT free `user_data`; the [moq_consume_video] `on_frame` callback
1453/// still fires once more with a terminal `0` (or a negative error), which is
1454/// where `user_data` should be released.
1455#[unsafe(no_mangle)]
1456pub extern "C" fn moq_consume_video_close(track: u32) -> i32 {
1457 ffi::enter(move || {
1458 let track = ffi::parse_id(track)?;
1459 State::lock().consume.track_close(track)
1460 })
1461}
1462
1463/// Consume an audio track from a broadcast, emitting the frames in order.
1464///
1465/// `on_frame` is called with a positive frame ID per frame, then exactly once
1466/// more with a terminal code: `0` (closed cleanly) or a negative error. After
1467/// the terminal (`<= 0`) callback, `on_frame` is never called again and
1468/// `user_data` is never touched again, so release `user_data` there. The
1469/// terminal callback fires even after [moq_consume_audio_close].
1470/// The `max_latency_ms` parameter controls how long to wait before skipping frames.
1471///
1472/// Returns a non-zero handle to the track on success, or a negative code on failure.
1473///
1474/// # Safety
1475/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
1476#[unsafe(no_mangle)]
1477pub unsafe extern "C" fn moq_consume_audio(
1478 catalog: u32,
1479 index: u32,
1480 max_latency_ms: u64,
1481 on_frame: Option<extern "C" fn(user_data: *mut c_void, frame: i32)>,
1482 user_data: *mut c_void,
1483) -> i32 {
1484 ffi::enter(move || {
1485 let catalog = ffi::parse_id(catalog)?;
1486 let index = index as usize;
1487 let max_latency = std::time::Duration::from_millis(max_latency_ms);
1488 let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame) };
1489 State::lock().consume.audio(catalog, index, max_latency, on_frame)
1490 })
1491}
1492
1493/// Stop an audio track consumer's background task.
1494///
1495/// Returns immediately: zero on success, or a negative code if already closed.
1496/// Does NOT free `user_data`; the [moq_consume_audio] `on_frame` callback
1497/// still fires once more with a terminal `0` (or a negative error), which is
1498/// where `user_data` should be released.
1499#[unsafe(no_mangle)]
1500pub extern "C" fn moq_consume_audio_close(track: u32) -> i32 {
1501 ffi::enter(move || {
1502 let track = ffi::parse_id(track)?;
1503 State::lock().consume.track_close(track)
1504 })
1505}
1506
1507/// Get a chunk of a frame's payload.
1508///
1509/// Read the payload of a frame as a single contiguous slice.
1510///
1511/// Frames are not chunked; the entire payload is delivered through `dst.payload` /
1512/// `dst.payload_size` in one call. The pointer is valid until [`moq_consume_frame_free`]
1513/// is called for this frame.
1514///
1515/// Returns a zero on success, or a negative code on failure.
1516///
1517/// # Safety
1518/// - The caller must ensure that `dst` is a valid pointer to a [moq_frame] struct.
1519#[unsafe(no_mangle)]
1520pub unsafe extern "C" fn moq_consume_frame(frame: u32, dst: *mut moq_frame) -> i32 {
1521 ffi::enter(move || {
1522 let frame = ffi::parse_id(frame)?;
1523 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1524 State::lock().consume.frame(frame, dst)
1525 })
1526}
1527
1528/// Free a decoded frame delivered via a [moq_consume_video] or [moq_consume_audio] callback.
1529///
1530/// Returns a zero on success, or a negative code on failure.
1531#[unsafe(no_mangle)]
1532pub extern "C" fn moq_consume_frame_free(frame: u32) -> i32 {
1533 ffi::enter(move || {
1534 let frame = ffi::parse_id(frame)?;
1535 State::lock().consume.frame_close(frame)
1536 })
1537}
1538
1539/// Close a broadcast consumer and clean up its resources.
1540///
1541/// Returns a zero on success, or a negative code on failure.
1542#[unsafe(no_mangle)]
1543pub extern "C" fn moq_consume_close(consume: u32) -> i32 {
1544 ffi::enter(move || {
1545 let consume = ffi::parse_id(consume)?;
1546 State::lock().consume.close(consume)
1547 })
1548}
1549
1550/// Subscribe to a raw track by name, delivering each frame's payload as-is.
1551///
1552/// This is the counterpart to [moq_publish_track]: no catalog lookup or
1553/// container parsing. `on_frame` is called with a positive raw frame ID for each
1554/// frame in sequence order, then exactly once more with a terminal code: `0`
1555/// (closed cleanly) or a negative error. After the terminal (`<= 0`) callback,
1556/// `on_frame` is never called again and `user_data` is never touched again, so
1557/// release `user_data` there. The terminal callback fires even after
1558/// [moq_consume_track_close]. Read each frame with [moq_consume_track_frame] and
1559/// release it with [moq_consume_track_frame_free]. Pass NULL for `subscription`
1560/// to use moq-net defaults.
1561///
1562/// Returns a non-zero handle to the track on success, or a negative code on failure.
1563///
1564/// # Safety
1565/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
1566/// - The caller must ensure that subscription is either NULL or a valid pointer to a [moq_subscription] struct.
1567/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
1568#[unsafe(no_mangle)]
1569pub unsafe extern "C" fn moq_consume_track(
1570 broadcast: u32,
1571 name: *const c_char,
1572 name_len: usize,
1573 subscription: *const moq_subscription,
1574 on_frame: Option<extern "C" fn(user_data: *mut c_void, frame: i32)>,
1575 user_data: *mut c_void,
1576) -> i32 {
1577 ffi::enter(move || {
1578 let broadcast = ffi::parse_id(broadcast)?;
1579 let name = unsafe { ffi::parse_str(name, name_len)? };
1580 let subscription = unsafe { subscription.as_ref() }.map(moq_net::track::Subscription::from);
1581 let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame) };
1582 State::lock().consume.raw_track(broadcast, name, subscription, on_frame)
1583 })
1584}
1585
1586/// Update a raw track subscription's delivery preferences.
1587///
1588/// Pass NULL for `subscription` to reset to moq-net defaults.
1589///
1590/// Returns a zero on success, or a negative code on failure.
1591///
1592/// # Safety
1593/// - The caller must ensure that subscription is either NULL or a valid pointer to a [moq_subscription] struct.
1594#[unsafe(no_mangle)]
1595pub unsafe extern "C" fn moq_consume_track_update(track: u32, subscription: *const moq_subscription) -> i32 {
1596 ffi::enter(move || {
1597 let track = ffi::parse_id(track)?;
1598 let subscription = unsafe { subscription.as_ref() }.map(moq_net::track::Subscription::from);
1599 State::lock().consume.raw_track_update(track, subscription)
1600 })
1601}
1602
1603/// Read a raw frame's payload delivered via the [moq_consume_track] callback.
1604///
1605/// Fills `dst.payload` / `dst.payload_size`; the pointer is valid until the
1606/// frame is released with [moq_consume_frame_free]. `dst.timestamp_us` is the
1607/// frame presentation timestamp in microseconds. `dst.keyframe` is reported as
1608/// false because raw tracks do not parse codec metadata.
1609///
1610/// Returns a zero on success, or a negative code on failure.
1611///
1612/// # Safety
1613/// - The caller must ensure that `dst` is a valid pointer to a [moq_frame] struct.
1614#[unsafe(no_mangle)]
1615pub unsafe extern "C" fn moq_consume_track_frame(frame: u32, dst: *mut moq_frame) -> i32 {
1616 ffi::enter(move || {
1617 let frame = ffi::parse_id(frame)?;
1618 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1619 State::lock().consume.raw_frame(frame, dst)
1620 })
1621}
1622
1623/// Free a raw frame delivered via the [moq_consume_track] callback, releasing its payload.
1624///
1625/// Returns a zero on success, or a negative code on failure.
1626#[unsafe(no_mangle)]
1627pub extern "C" fn moq_consume_track_frame_free(frame: u32) -> i32 {
1628 ffi::enter(move || {
1629 let frame = ffi::parse_id(frame)?;
1630 State::lock().consume.raw_frame_close(frame)
1631 })
1632}
1633
1634/// Stop a raw track consumer's background task.
1635///
1636/// Returns immediately: zero on success, or a negative code if already closed.
1637/// Does NOT free `user_data`; the [moq_consume_track] `on_frame` callback still
1638/// fires once more with a terminal `0` (or a negative error), which is where
1639/// `user_data` should be released. Frames already delivered via the callback
1640/// remain valid until released with [moq_consume_track_frame_free].
1641#[unsafe(no_mangle)]
1642pub extern "C" fn moq_consume_track_close(track: u32) -> i32 {
1643 ffi::enter(move || {
1644 let track = ffi::parse_id(track)?;
1645 State::lock().consume.raw_track_close(track)
1646 })
1647}
1648
1649/// Subscribe to a raw track's best-effort datagrams by name.
1650///
1651/// The datagram counterpart to [moq_consume_track], on its own subscription. `on_datagram`
1652/// is called with a positive datagram ID for each datagram in arrival order, then exactly
1653/// once more with a terminal code: `0` (closed cleanly) or a negative error. After the
1654/// terminal (`<= 0`) callback, `on_datagram` is never called again and `user_data` is never
1655/// touched again, so release `user_data` there. The terminal callback fires even after
1656/// [moq_consume_datagrams_close]. Read each datagram with [moq_consume_datagram] and release
1657/// it with [moq_consume_datagram_free]. Datagrams arrive only over datagram-capable
1658/// transports and lite-05 or newer moq-lite; there is no stream fallback.
1659///
1660/// Returns a non-zero handle to the subscription on success, or a negative code on failure.
1661///
1662/// # Safety
1663/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
1664/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_datagram` callback.
1665#[unsafe(no_mangle)]
1666pub unsafe extern "C" fn moq_consume_datagrams(
1667 broadcast: u32,
1668 name: *const c_char,
1669 name_len: usize,
1670 on_datagram: Option<extern "C" fn(user_data: *mut c_void, datagram: i32)>,
1671 user_data: *mut c_void,
1672) -> i32 {
1673 ffi::enter(move || {
1674 let broadcast = ffi::parse_id(broadcast)?;
1675 let name = unsafe { ffi::parse_str(name, name_len)? };
1676 let on_datagram = unsafe { ffi::OnStatus::new(user_data, on_datagram) };
1677 State::lock().consume.datagram_track(broadcast, name, on_datagram)
1678 })
1679}
1680
1681/// Read a datagram delivered via the [moq_consume_datagrams] callback.
1682///
1683/// Fills `dst.payload` / `dst.payload_size` (valid until the datagram is released with
1684/// [moq_consume_datagram_free]), plus `dst.timestamp_us` and `dst.sequence`.
1685///
1686/// Returns a zero on success, or a negative code on failure.
1687///
1688/// # Safety
1689/// - The caller must ensure that `dst` is a valid pointer to a [moq_datagram] struct.
1690#[unsafe(no_mangle)]
1691pub unsafe extern "C" fn moq_consume_datagram(datagram: u32, dst: *mut moq_datagram) -> i32 {
1692 ffi::enter(move || {
1693 let datagram = ffi::parse_id(datagram)?;
1694 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1695 State::lock().consume.datagram(datagram, dst)
1696 })
1697}
1698
1699/// Free a datagram delivered via the [moq_consume_datagrams] callback, releasing its payload.
1700///
1701/// Returns a zero on success, or a negative code on failure.
1702#[unsafe(no_mangle)]
1703pub extern "C" fn moq_consume_datagram_free(datagram: u32) -> i32 {
1704 ffi::enter(move || {
1705 let datagram = ffi::parse_id(datagram)?;
1706 State::lock().consume.datagram_close(datagram)
1707 })
1708}
1709
1710/// Stop a datagram subscription's background task.
1711///
1712/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
1713/// `user_data`; the [moq_consume_datagrams] `on_datagram` callback still fires once more with a
1714/// terminal `0` (or a negative error), which is where `user_data` should be released. Datagrams
1715/// already delivered via the callback remain valid until released with [moq_consume_datagram_free].
1716#[unsafe(no_mangle)]
1717pub extern "C" fn moq_consume_datagrams_close(task: u32) -> i32 {
1718 ffi::enter(move || {
1719 let task = ffi::parse_id(task)?;
1720 State::lock().consume.datagram_track_close(task)
1721 })
1722}
1723
1724/// Subscribe to a JSON snapshot track (lossy latest-value) by name.
1725///
1726/// `on_value` is called with a positive value ID for each new latest value; a consumer that
1727/// falls behind collapses the backlog and only sees the newest. It is called exactly once more
1728/// with a terminal `0` (track ended / closed) or a negative error, after which `user_data` is
1729/// never touched again, so release it there. Read each value with [moq_consume_json_value] and
1730/// release it with [moq_consume_json_value_free]. Pass the same compression the producer used.
1731///
1732/// Returns a non-zero handle to the task on success, or a negative code on failure.
1733///
1734/// # Safety
1735/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
1736/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_value` callback.
1737#[unsafe(no_mangle)]
1738pub unsafe extern "C" fn moq_consume_json_snapshot(
1739 broadcast: u32,
1740 name: *const c_char,
1741 name_len: usize,
1742 config: *const moq_json_snapshot_config,
1743 on_value: Option<extern "C" fn(user_data: *mut c_void, value: i32)>,
1744 user_data: *mut c_void,
1745) -> i32 {
1746 ffi::enter(move || {
1747 let broadcast = ffi::parse_id(broadcast)?;
1748 let name = unsafe { ffi::parse_str(name, name_len)? };
1749 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1750 let mut consumer = moq_json::snapshot::ConsumerConfig::default();
1751 consumer.compression = config.compression;
1752 let on_value = unsafe { ffi::OnStatus::new(user_data, on_value) };
1753 State::lock().consume.json_snapshot(broadcast, name, consumer, on_value)
1754 })
1755}
1756
1757/// Subscribe to a JSON stream track (lossless append-log) by name.
1758///
1759/// `on_value` is called with a positive value ID for each record, in order, then once more with
1760/// a terminal `0` or negative error where `user_data` should be released. Read each value with
1761/// [moq_consume_json_value] and release it with [moq_consume_json_value_free].
1762///
1763/// Returns a non-zero handle to the task on success, or a negative code on failure.
1764///
1765/// # Safety
1766/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
1767/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_value` callback.
1768#[unsafe(no_mangle)]
1769pub unsafe extern "C" fn moq_consume_json_stream(
1770 broadcast: u32,
1771 name: *const c_char,
1772 name_len: usize,
1773 config: *const moq_json_stream_config,
1774 on_value: Option<extern "C" fn(user_data: *mut c_void, value: i32)>,
1775 user_data: *mut c_void,
1776) -> i32 {
1777 ffi::enter(move || {
1778 let broadcast = ffi::parse_id(broadcast)?;
1779 let name = unsafe { ffi::parse_str(name, name_len)? };
1780 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1781 let consumer = moq_json::stream::ConsumerConfig::default().with_compression(config.compression);
1782 let on_value = unsafe { ffi::OnStatus::new(user_data, on_value) };
1783 State::lock().consume.json_stream(broadcast, name, consumer, on_value)
1784 })
1785}
1786
1787/// Read a JSON value delivered via a [moq_consume_json_snapshot] or [moq_consume_json_stream] callback.
1788///
1789/// Fills `dst.json` / `dst.json_len`; the pointer is valid until the value is released with
1790/// [moq_consume_json_value_free].
1791///
1792/// Returns a zero on success, or a negative code on failure.
1793///
1794/// # Safety
1795/// - The caller must ensure `dst` is a valid pointer to a [moq_json_value] struct.
1796#[unsafe(no_mangle)]
1797pub unsafe extern "C" fn moq_consume_json_value(value: u32, dst: *mut moq_json_value) -> i32 {
1798 ffi::enter(move || {
1799 let value = ffi::parse_id(value)?;
1800 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1801 State::lock().consume.json_value(value, dst)
1802 })
1803}
1804
1805/// Release a JSON value delivered via a consumer callback.
1806///
1807/// Returns a zero on success, or a negative code on failure.
1808#[unsafe(no_mangle)]
1809pub extern "C" fn moq_consume_json_value_free(value: u32) -> i32 {
1810 ffi::enter(move || {
1811 let value = ffi::parse_id(value)?;
1812 State::lock().consume.json_value_close(value)
1813 })
1814}
1815
1816/// Stop a JSON consumer's background task (snapshot or stream).
1817///
1818/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
1819/// `user_data`; the `on_value` callback still fires once more with a terminal `0` (or a negative
1820/// error), which is where `user_data` should be released. Values already delivered remain valid
1821/// until released with [moq_consume_json_value_free].
1822#[unsafe(no_mangle)]
1823pub extern "C" fn moq_consume_json_close(task: u32) -> i32 {
1824 ffi::enter(move || {
1825 let task = ffi::parse_id(task)?;
1826 State::lock().consume.json_close(task)
1827 })
1828}