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