moq/api.rs
1use crate::{Connect, 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 in both directions. As an output (e.g. a JSON document libmoq hands back) the
244/// pointer borrows libmoq's own storage and is only valid until the owning resource is
245/// freed; see the function that fills it for the exact lifetime. As an input (e.g. a
246/// `moq_client_set_*` list) the pointer borrows the caller's storage and is only read
247/// during the call.
248#[repr(C)]
249#[allow(non_camel_case_types)]
250#[derive(Clone, Copy)]
251pub struct moq_string {
252 /// Pointer to `len` bytes of UTF-8, NOT NULL terminated.
253 pub data: *const c_char,
254 pub len: usize,
255}
256
257/// One untyped application catalog section: a name and its JSON value.
258///
259/// Both `name` and `json` are UTF-8, NOT NULL terminated, and borrow the catalog
260/// snapshot's storage. They stay valid until the snapshot is freed with
261/// [moq_consume_catalog_free]. `json` is the section's value serialized as JSON
262/// (parse it yourself); a top-level catalog key beyond `video`/`audio`.
263#[repr(C)]
264#[allow(non_camel_case_types)]
265pub struct moq_section {
266 /// The section name, NOT NULL terminated.
267 pub name: *const c_char,
268 pub name_len: usize,
269
270 /// The section value as a JSON document, NOT NULL terminated.
271 pub json: *const c_char,
272 pub json_len: usize,
273}
274
275/// Information about a broadcast announced by an origin.
276#[repr(C)]
277#[allow(non_camel_case_types)]
278pub struct moq_announced {
279 /// The path of the broadcast, NOT NULL terminated
280 pub path: *const c_char,
281 pub path_len: usize,
282
283 /// Whether the broadcast is active or has ended
284 /// This MUST toggle between true and false over the lifetime of the broadcast
285 pub active: bool,
286}
287
288/// A snapshot of connection statistics, filled in by [moq_session_stats].
289///
290/// Each metric has a `*_valid` flag: when `false`, the matching value is meaningless because
291/// the transport backend doesn't report it (a `false` flag is NOT the same as a zero value).
292/// Native QUIC reports every metric; the browser WebTransport reports few or none. Initialize
293/// the struct to zero before the call; [moq_session_stats] overwrites every field.
294#[repr(C)]
295#[allow(non_camel_case_types)]
296pub struct moq_connection_stats {
297 /// Smoothed round-trip time, in microseconds.
298 pub rtt_us: u64,
299 pub rtt_valid: bool,
300
301 /// Estimated send bandwidth from the congestion controller, in bits per second.
302 pub send_rate_bps: u64,
303 pub send_rate_valid: bool,
304
305 /// Estimated receive bandwidth from MoQ PROBE, in bits per second.
306 pub recv_rate_bps: u64,
307 pub recv_rate_valid: bool,
308
309 /// Total bytes sent, including retransmissions and overhead.
310 pub bytes_sent: u64,
311 pub bytes_sent_valid: bool,
312
313 /// Total bytes received, including duplicates and overhead.
314 pub bytes_received: u64,
315 pub bytes_received_valid: bool,
316
317 /// Total bytes lost (detected via retransmission or acknowledgement).
318 pub bytes_lost: u64,
319 pub bytes_lost_valid: bool,
320
321 /// Total datagrams sent.
322 pub packets_sent: u64,
323 pub packets_sent_valid: bool,
324
325 /// Total datagrams received.
326 pub packets_received: u64,
327 pub packets_received_valid: bool,
328
329 /// Total datagrams detected as lost.
330 pub packets_lost: u64,
331 pub packets_lost_valid: bool,
332}
333
334impl From<&moq_net::ConnectionStats> for moq_connection_stats {
335 fn from(stats: &moq_net::ConnectionStats) -> Self {
336 // An Option<u64> becomes a (value, valid) pair; absent metrics report 0/false.
337 fn split(value: Option<u64>) -> (u64, bool) {
338 (value.unwrap_or(0), value.is_some())
339 }
340
341 let (rtt_us, rtt_valid) = split(stats.rtt.map(|d| d.as_micros() as u64));
342 let (send_rate_bps, send_rate_valid) = split(stats.estimated_send_rate);
343 let (recv_rate_bps, recv_rate_valid) = split(stats.estimated_recv_rate);
344 let (bytes_sent, bytes_sent_valid) = split(stats.bytes_sent);
345 let (bytes_received, bytes_received_valid) = split(stats.bytes_received);
346 let (bytes_lost, bytes_lost_valid) = split(stats.bytes_lost);
347 let (packets_sent, packets_sent_valid) = split(stats.packets_sent);
348 let (packets_received, packets_received_valid) = split(stats.packets_received);
349 let (packets_lost, packets_lost_valid) = split(stats.packets_lost);
350
351 Self {
352 rtt_us,
353 rtt_valid,
354 send_rate_bps,
355 send_rate_valid,
356 recv_rate_bps,
357 recv_rate_valid,
358 bytes_sent,
359 bytes_sent_valid,
360 bytes_received,
361 bytes_received_valid,
362 bytes_lost,
363 bytes_lost_valid,
364 packets_sent,
365 packets_sent_valid,
366 packets_received,
367 packets_received_valid,
368 packets_lost,
369 packets_lost_valid,
370 }
371 }
372}
373
374/// Initialize the library with a log level.
375///
376/// This should be called before any other functions.
377/// The log_level is a string: "error", "warn", "info", "debug", "trace"
378///
379/// Returns a zero on success, or a negative code on failure.
380///
381/// # Safety
382/// - The caller must ensure that level is a valid pointer to level_len bytes of data.
383#[unsafe(no_mangle)]
384pub unsafe extern "C" fn moq_log_level(level: *const c_char, level_len: usize) -> i32 {
385 ffi::enter(move || {
386 match unsafe { ffi::parse_str(level, level_len)? } {
387 "" => moq_native::Log::default(),
388 level => moq_native::Log::new(Level::from_str(level)?),
389 }
390 .init()?;
391
392 Ok(())
393 })
394}
395
396/// Human-readable reason for the most recent failed call on the calling thread.
397///
398/// libmoq functions return only a negative code; this exposes the matching message
399/// (including detail the code can't carry, e.g. which URL failed to parse or why a
400/// decode failed). The string is only meaningful after a call returned a negative
401/// code; check the code first.
402///
403/// Returns a NUL-terminated, UTF-8 pointer valid until the next libmoq call **on the
404/// same thread**, or NULL if no error has been recorded on this thread. Copy it if you
405/// need it to outlive the next call. Errors delivered through status callbacks carry
406/// their code directly; read this from inside the callback to get their reason.
407#[unsafe(no_mangle)]
408pub extern "C" fn moq_error() -> *const c_char {
409 ffi::last_error_ptr()
410}
411
412/// The protocol version names this build offers by default, spelled the way
413/// [moq_client_set_versions] expects. Built once; the slices are valid for the life of
414/// the process.
415static VERSION_NAMES: std::sync::LazyLock<Vec<String>> =
416 std::sync::LazyLock::new(|| moq_net::Versions::all().iter().map(|v| v.to_string()).collect());
417
418/// List the protocol versions offered during the handshake by default.
419///
420/// Writes up to `count` names into `dst` and returns the total number available, which
421/// may be larger than `count`. Pass a NULL `dst` with a zero `count` to size the array
422/// first. Each name borrows a static string valid for the life of the process, so a
423/// caller building a menu can hold them indefinitely.
424///
425/// Work-in-progress versions are omitted, since they are not advertised unless pinned;
426/// [moq_client_set_versions] still accepts them by name.
427///
428/// Returns the total count on success, or a negative code on failure.
429///
430/// # Safety
431/// - The caller must ensure that `dst` is either NULL with a zero `count`, or a valid
432/// pointer to `count` writable [moq_string] values.
433#[unsafe(no_mangle)]
434pub unsafe extern "C" fn moq_versions(dst: *mut moq_string, count: usize) -> i32 {
435 ffi::enter(move || {
436 if !dst.is_null() {
437 let dst = unsafe { std::slice::from_raw_parts_mut(dst, count) };
438 for (slot, name) in dst.iter_mut().zip(VERSION_NAMES.iter()) {
439 slot.data = name.as_ptr().cast::<c_char>();
440 slot.len = name.len();
441 }
442 } else if count != 0 {
443 return Err(Error::InvalidPointer);
444 }
445
446 Ok(VERSION_NAMES.len())
447 })
448}
449
450/// The QUIC backend names this build offers, spelled the way [moq_client_set_backend]
451/// expects. Built once; the slices are valid for the life of the process.
452static BACKEND_NAMES: std::sync::LazyLock<Vec<&'static str>> =
453 std::sync::LazyLock::new(|| moq_native::QuicBackend::compiled().iter().map(|b| b.as_str()).collect());
454
455/// List the QUIC backends this build was compiled with.
456///
457/// Writes up to `count` names into `dst` and returns the total number available, which
458/// may be larger than `count`. Pass a NULL `dst` with a zero `count` to size the array
459/// first. Each name borrows a static string valid for the life of the process.
460///
461/// The backends are compile-time optional, so a caller building a menu must read this
462/// rather than listing names: an option this build lacks is rejected by
463/// [moq_client_set_backend], which would leave a menu entry that can only fail.
464///
465/// Returns the total count on success, or a negative code on failure.
466///
467/// # Safety
468/// - The caller must ensure that `dst` is either NULL with a zero `count`, or a valid
469/// pointer to `count` writable [moq_string] values.
470#[unsafe(no_mangle)]
471pub unsafe extern "C" fn moq_backends(dst: *mut moq_string, count: usize) -> i32 {
472 ffi::enter(move || {
473 if !dst.is_null() {
474 let dst = unsafe { std::slice::from_raw_parts_mut(dst, count) };
475 for (slot, name) in dst.iter_mut().zip(BACKEND_NAMES.iter()) {
476 slot.data = name.as_ptr().cast::<c_char>();
477 slot.len = name.len();
478 }
479 } else if count != 0 {
480 return Err(Error::InvalidPointer);
481 }
482
483 Ok(BACKEND_NAMES.len())
484 })
485}
486
487/// Whether this build can capture qlog traces.
488///
489/// Capture is compile-time optional. [moq_client_set_quic_qlog] accepts a directory
490/// either way, but dialing fails when the support is absent, so a caller offering the
491/// knob should hide it rather than surface an option that cannot work.
492#[unsafe(no_mangle)]
493pub extern "C" fn moq_qlog_supported() -> bool {
494 moq_native::qlog_supported()
495}
496
497/// A duration as the milliseconds the setters take, saturating rather than wrapping.
498fn millis(duration: std::time::Duration) -> u64 {
499 duration.as_millis().min(u64::MAX as u128) as u64
500}
501
502/// Create a client configuration for [moq_client_connect].
503///
504/// A fresh handle carries the same defaults [moq_session_connect] dials with; the
505/// `moq_client_set_*` functions override one knob at a time. Connecting clones the
506/// config, so one handle can open any number of sessions and stays editable in between.
507///
508/// Returns a non-zero handle on success, or a negative code on failure. Release it with
509/// [moq_client_close]; that does not disturb sessions already dialed from it.
510#[unsafe(no_mangle)]
511pub extern "C" fn moq_client_create() -> i32 {
512 ffi::enter(move || State::lock().client.create())
513}
514
515/// Release a client configuration created by [moq_client_create].
516///
517/// Sessions already dialed from it keep running: each connect took its own copy.
518///
519/// Returns zero on success, or a negative code if the handle is unknown.
520#[unsafe(no_mangle)]
521pub extern "C" fn moq_client_close(client: u32) -> i32 {
522 ffi::enter(move || {
523 let client = ffi::parse_id(client)?;
524 State::lock().client.close(client)
525 })
526}
527
528/// Restrict the protocol versions offered during the handshake.
529///
530/// By default every supported version is offered and the server picks one. Pass a
531/// subset to pin the negotiation, in the same spelling the CLI uses: `moq-lite-01`
532/// through `moq-lite-06-wip`, or `moq-transport-14` through `moq-transport-19`. An
533/// empty list restores the default.
534///
535/// Returns zero on success, or a negative code if the handle is unknown or a version
536/// string is unrecognized.
537///
538/// # Safety
539/// - The caller must ensure that `versions` is either NULL with a zero `count`, or a
540/// valid pointer to `count` [moq_string] values, each valid for its own length.
541#[unsafe(no_mangle)]
542pub unsafe extern "C" fn moq_client_set_versions(client: u32, versions: *const moq_string, count: usize) -> i32 {
543 ffi::enter(move || {
544 let client = ffi::parse_id(client)?;
545 let versions = unsafe { ffi::parse_strings(versions, count)? }
546 .into_iter()
547 .map(|version| moq_net::Version::from_str(&version).map_err(Error::InvalidConfig))
548 .collect::<Result<Vec<_>, Error>>()?;
549
550 State::lock().client.get_mut(client)?.version = versions;
551 Ok(())
552 })
553}
554
555/// Choose the QUIC backend: `"quinn"`, `"quiche"`, or `"noq"`.
556///
557/// Defaults to whichever is compiled in, preferring quinn. A NULL or empty value
558/// restores that auto-detection.
559///
560/// Returns zero on success, or a negative code if the handle is unknown or the backend
561/// is unrecognized (which includes a backend this build was compiled without).
562///
563/// # Safety
564/// - The caller must ensure that `backend` is NULL or a valid pointer to `backend_len` bytes.
565#[unsafe(no_mangle)]
566pub unsafe extern "C" fn moq_client_set_backend(client: u32, backend: *const c_char, backend_len: usize) -> i32 {
567 ffi::enter(move || {
568 let backend = match unsafe { ffi::parse_str_optional(backend, backend_len)? } {
569 Some(backend) => Some(moq_native::QuicBackend::from_str(backend).map_err(Error::InvalidConfig)?),
570 None => None,
571 };
572
573 let client = ffi::parse_id(client)?;
574 State::lock().client.get_mut(client)?.backend = backend;
575 Ok(())
576 })
577}
578
579/// Set the local UDP socket address to bind, e.g. `"[::]:0"` (the default) or
580/// `"192.0.2.7:0"` to pin the outgoing interface.
581///
582/// Returns zero on success, or a negative code if the handle is unknown or the address
583/// does not parse.
584///
585/// # Safety
586/// - The caller must ensure that `addr` is a valid pointer to `addr_len` bytes.
587#[unsafe(no_mangle)]
588pub unsafe extern "C" fn moq_client_set_bind(client: u32, addr: *const c_char, addr_len: usize) -> i32 {
589 ffi::enter(move || {
590 let addr = unsafe { ffi::parse_str(addr, addr_len)? };
591 let addr: std::net::SocketAddr = addr
592 .parse()
593 .map_err(|err| Error::InvalidConfig(format!("invalid bind address {addr:?}: {err}")))?;
594
595 let client = ffi::parse_id(client)?;
596 State::lock().client.get_mut(client)?.bind = addr;
597 Ok(())
598 })
599}
600
601/// Bound one connection attempt, covering both the dial and the MoQ handshake, in
602/// milliseconds.
603///
604/// Defaults to 30s; zero waits forever. The reconnect loop only re-arms its backoff
605/// between attempts, so this is what stops a peer that accepts the connection and then
606/// never speaks from wedging the loop.
607///
608/// Returns zero on success, or a negative code if the handle is unknown.
609#[unsafe(no_mangle)]
610pub extern "C" fn moq_client_set_connect_timeout(client: u32, timeout_ms: u64) -> i32 {
611 ffi::enter(move || {
612 let client = ffi::parse_id(client)?;
613 State::lock().client.get_mut(client)?.timeout = Some(std::time::Duration::from_millis(timeout_ms));
614 Ok(())
615 })
616}
617
618/// Delay before also dialing the next resolved address (Happy Eyeballs), in milliseconds.
619///
620/// When DNS returns several addresses, attempts alternate between IPv6 and IPv4, each
621/// starting this long after the previous one, and the first to complete wins. Defaults
622/// to 250ms; zero dials every address at once.
623///
624/// Returns zero on success, or a negative code if the handle is unknown.
625#[unsafe(no_mangle)]
626pub extern "C" fn moq_client_set_failover_delay(client: u32, delay_ms: u64) -> i32 {
627 ffi::enter(move || {
628 let client = ffi::parse_id(client)?;
629 State::lock().client.get_mut(client)?.failover_delay = Some(std::time::Duration::from_millis(delay_ms));
630 Ok(())
631 })
632}
633
634/// Delay before racing a WebSocket fallback against the QUIC dial, in milliseconds.
635///
636/// Defaults to 200ms, and drops to zero for a server WebSocket already won against.
637/// This is what gets a publisher through a network that blocks UDP.
638///
639/// Returns zero on success, or a negative code if the handle is unknown.
640#[unsafe(no_mangle)]
641pub extern "C" fn moq_client_set_websocket_delay(client: u32, delay_ms: u64) -> i32 {
642 ffi::enter(move || {
643 let client = ffi::parse_id(client)?;
644 State::lock().client.get_mut(client)?.websocket.delay = Some(std::time::Duration::from_millis(delay_ms));
645 Ok(())
646 })
647}
648
649/// Enable or disable the WebSocket fallback entirely.
650///
651/// Enabled by default. Disabling it makes a UDP-blocked network fail outright rather
652/// than falling back, which is what you want when measuring the QUIC path.
653///
654/// Returns zero on success, or a negative code if the handle is unknown.
655#[unsafe(no_mangle)]
656pub extern "C" fn moq_client_set_websocket_enabled(client: u32, enabled: bool) -> i32 {
657 ffi::enter(move || {
658 let client = ffi::parse_id(client)?;
659 State::lock().client.get_mut(client)?.websocket.enabled = enabled;
660 Ok(())
661 })
662}
663
664/// Skip TLS certificate verification.
665///
666/// Development only: it accepts any certificate, so it defeats the point of TLS. Prefer
667/// [moq_client_set_tls_fingerprints] to trust one known self-signed certificate.
668///
669/// Returns zero on success, or a negative code if the handle is unknown.
670#[unsafe(no_mangle)]
671pub extern "C" fn moq_client_set_tls_disable_verify(client: u32, disable: bool) -> i32 {
672 ffi::enter(move || {
673 let client = ffi::parse_id(client)?;
674 State::lock().client.get_mut(client)?.tls.disable_verify = Some(disable);
675 Ok(())
676 })
677}
678
679/// Whether to also trust the platform's native root certificates.
680///
681/// By default the system roots are trusted only when no custom roots are configured.
682/// Set this to true to trust them alongside the roots from [moq_client_set_tls_roots],
683/// or false to trust only those.
684///
685/// Returns zero on success, or a negative code if the handle is unknown.
686#[unsafe(no_mangle)]
687pub extern "C" fn moq_client_set_tls_system_roots(client: u32, enabled: bool) -> i32 {
688 ffi::enter(move || {
689 let client = ffi::parse_id(client)?;
690 State::lock().client.get_mut(client)?.tls.system_roots = Some(enabled);
691 Ok(())
692 })
693}
694
695/// Trust these PEM root certificate files.
696///
697/// An empty list restores the default of using the platform's native root store.
698///
699/// Returns zero on success, or a negative code if the handle is unknown.
700///
701/// # Safety
702/// - The caller must ensure that `paths` is either NULL with a zero `count`, or a valid
703/// pointer to `count` [moq_string] values, each valid for its own length.
704#[unsafe(no_mangle)]
705pub unsafe extern "C" fn moq_client_set_tls_roots(client: u32, paths: *const moq_string, count: usize) -> i32 {
706 ffi::enter(move || {
707 let paths = unsafe { ffi::parse_strings(paths, count)? };
708 let client = ffi::parse_id(client)?;
709 State::lock().client.get_mut(client)?.tls.root = paths.into_iter().map(Into::into).collect();
710 Ok(())
711 })
712}
713
714/// Pin the peer to a certificate with one of these SHA-256 fingerprints, hex encoded.
715///
716/// The native equivalent of the browser's WebTransport `serverCertificateHashes`, taking
717/// the same values a relay reports for its self-signed certificate. Use it instead of
718/// [moq_client_set_tls_disable_verify] to trust one known certificate without accepting
719/// every certificate. An empty list clears any pinned fingerprints.
720///
721/// Returns zero on success, or a negative code if the handle is unknown.
722///
723/// # Safety
724/// - The caller must ensure that `fingerprints` is either NULL with a zero `count`, or a
725/// valid pointer to `count` [moq_string] values, each valid for its own length.
726#[unsafe(no_mangle)]
727pub unsafe extern "C" fn moq_client_set_tls_fingerprints(
728 client: u32,
729 fingerprints: *const moq_string,
730 count: usize,
731) -> i32 {
732 ffi::enter(move || {
733 let fingerprints = unsafe { ffi::parse_strings(fingerprints, count)? };
734 for fingerprint in &fingerprints {
735 moq_native::tls::parse_fingerprint(fingerprint).map_err(|err| Error::InvalidConfig(err.to_string()))?;
736 }
737 let client = ffi::parse_id(client)?;
738 State::lock().client.get_mut(client)?.tls.fingerprint = fingerprints;
739 Ok(())
740 })
741}
742
743/// Override the TLS server name (SNI) sent during the handshake.
744///
745/// Defaults to the host in the dial URL. Set this to reach a relay by IP while still
746/// validating its certificate against the name it was issued for. A NULL or empty value
747/// restores the default.
748///
749/// Returns zero on success, or a negative code if the handle is unknown.
750///
751/// # Safety
752/// - The caller must ensure that `name` is NULL or a valid pointer to `name_len` bytes.
753#[unsafe(no_mangle)]
754pub unsafe extern "C" fn moq_client_set_tls_host_name(client: u32, name: *const c_char, name_len: usize) -> i32 {
755 ffi::enter(move || {
756 let name = unsafe { ffi::parse_str_optional(name, name_len)? }.map(str::to_string);
757 let client = ffi::parse_id(client)?;
758 State::lock().client.get_mut(client)?.tls.host_name = name;
759 Ok(())
760 })
761}
762
763/// Present this PEM certificate chain when the relay requires mTLS.
764///
765/// Only certificates are read from the file; any private keys in it are ignored. Must be
766/// paired with [moq_client_set_tls_key] or the connect fails. A NULL or empty path clears it.
767///
768/// Returns zero on success, or a negative code if the handle is unknown.
769///
770/// # Safety
771/// - The caller must ensure that `path` is NULL or a valid pointer to `path_len` bytes.
772#[unsafe(no_mangle)]
773pub unsafe extern "C" fn moq_client_set_tls_cert(client: u32, path: *const c_char, path_len: usize) -> i32 {
774 ffi::enter(move || {
775 let path = unsafe { ffi::parse_str_optional(path, path_len)? }.map(Into::into);
776 let client = ffi::parse_id(client)?;
777 State::lock().client.get_mut(client)?.tls.cert = path;
778 Ok(())
779 })
780}
781
782/// Present this PEM private key when the relay requires mTLS.
783///
784/// Only the private key is read from the file; any certificates in it are ignored. Must
785/// be paired with [moq_client_set_tls_cert] or the connect fails. A NULL or empty path
786/// clears it.
787///
788/// Returns zero on success, or a negative code if the handle is unknown.
789///
790/// # Safety
791/// - The caller must ensure that `path` is NULL or a valid pointer to `path_len` bytes.
792#[unsafe(no_mangle)]
793pub unsafe extern "C" fn moq_client_set_tls_key(client: u32, path: *const c_char, path_len: usize) -> i32 {
794 ffi::enter(move || {
795 let path = unsafe { ffi::parse_str_optional(path, path_len)? }.map(Into::into);
796 let client = ffi::parse_id(client)?;
797 State::lock().client.get_mut(client)?.tls.key = path;
798 Ok(())
799 })
800}
801
802/// Set the delay before the first reconnect attempt, in milliseconds.
803///
804/// The delay grows from here by the multiplier after each failure. Defaults to 1s.
805///
806/// Returns zero on success, or a negative code if the handle is unknown.
807#[unsafe(no_mangle)]
808pub extern "C" fn moq_client_set_backoff_initial(client: u32, delay_ms: u64) -> i32 {
809 ffi::enter(move || {
810 let client = ffi::parse_id(client)?;
811 State::lock().client.get_mut(client)?.backoff.initial = std::time::Duration::from_millis(delay_ms);
812 Ok(())
813 })
814}
815
816/// Set the multiplier applied to the reconnect delay after each failed attempt.
817///
818/// Defaults to 2. A multiplier of 1 keeps the delay flat.
819///
820/// Returns zero on success, or a negative code if the handle is unknown.
821#[unsafe(no_mangle)]
822pub extern "C" fn moq_client_set_backoff_multiplier(client: u32, multiplier: u32) -> i32 {
823 ffi::enter(move || {
824 let client = ffi::parse_id(client)?;
825 State::lock().client.get_mut(client)?.backoff.multiplier = multiplier;
826 Ok(())
827 })
828}
829
830/// Set the ceiling on the growing reconnect delay, in milliseconds.
831///
832/// Defaults to 5s.
833///
834/// Returns zero on success, or a negative code if the handle is unknown.
835#[unsafe(no_mangle)]
836pub extern "C" fn moq_client_set_backoff_max(client: u32, delay_ms: u64) -> i32 {
837 ffi::enter(move || {
838 let client = ffi::parse_id(client)?;
839 State::lock().client.get_mut(client)?.backoff.max = std::time::Duration::from_millis(delay_ms);
840 Ok(())
841 })
842}
843
844/// Set how long to keep retrying before giving up, in milliseconds.
845///
846/// Zero retries forever. Defaults to 10s. This is also how long published
847/// broadcasts linger across a drop, so a longer timeout papers over a longer relay
848/// outage.
849///
850/// Returns zero on success, or a negative code if the handle is unknown.
851#[unsafe(no_mangle)]
852pub extern "C" fn moq_client_set_backoff_timeout(client: u32, timeout_ms: u64) -> i32 {
853 ffi::enter(move || {
854 let client = ffi::parse_id(client)?;
855 State::lock().client.get_mut(client)?.backoff.timeout = std::time::Duration::from_millis(timeout_ms);
856 Ok(())
857 })
858}
859
860/// Set the maximum concurrent QUIC streams per connection, bidirectional and
861/// unidirectional alike.
862///
863/// Defaults to 1024. MoQ opens a stream per group, so a busy publisher wants this high.
864/// QUIC only; the WebSocket fallback ignores it.
865///
866/// Returns zero on success, or a negative code if the handle is unknown.
867#[unsafe(no_mangle)]
868pub extern "C" fn moq_client_set_quic_max_streams(client: u32, max_streams: u64) -> i32 {
869 ffi::enter(move || {
870 let client = ffi::parse_id(client)?;
871 State::lock().client.get_mut(client)?.quic.max_streams = Some(max_streams);
872 Ok(())
873 })
874}
875
876/// Set the idle timeout before an inactive connection is dropped, in milliseconds.
877///
878/// Defaults to 30s. QUIC carries this as a millisecond varint, so a value of 2^62 or
879/// more is rejected when the connection is dialed. QUIC only.
880///
881/// Returns zero on success, or a negative code if the handle is unknown.
882#[unsafe(no_mangle)]
883pub extern "C" fn moq_client_set_quic_idle_timeout(client: u32, timeout_ms: u64) -> i32 {
884 ffi::enter(move || {
885 let client = ffi::parse_id(client)?;
886 State::lock().client.get_mut(client)?.quic.idle_timeout = Some(std::time::Duration::from_millis(timeout_ms));
887 Ok(())
888 })
889}
890
891/// Set the keep-alive ping interval, in milliseconds.
892///
893/// Defaults to 5s; zero disables the pings. QUIC only.
894///
895/// Returns zero on success, or a negative code if the handle is unknown.
896#[unsafe(no_mangle)]
897pub extern "C" fn moq_client_set_quic_keep_alive(client: u32, interval_ms: u64) -> i32 {
898 ffi::enter(move || {
899 let client = ffi::parse_id(client)?;
900 State::lock().client.get_mut(client)?.quic.keep_alive = Some(std::time::Duration::from_millis(interval_ms));
901 Ok(())
902 })
903}
904
905/// Enable or disable UDP generic segmentation offload.
906///
907/// GSO batches sends into one syscall for throughput, and defaults to on. Some NICs and
908/// middleboxes mangle segmented packets, so turn it off if large sends vanish. QUIC only.
909///
910/// Returns zero on success, or a negative code if the handle is unknown.
911#[unsafe(no_mangle)]
912pub extern "C" fn moq_client_set_quic_gso(client: u32, enabled: bool) -> i32 {
913 ffi::enter(move || {
914 let client = ffi::parse_id(client)?;
915 State::lock().client.get_mut(client)?.quic.gso = Some(enabled);
916 Ok(())
917 })
918}
919
920/// Enable or disable path MTU discovery.
921///
922/// Defaults to off. QUIC only.
923///
924/// Returns zero on success, or a negative code if the handle is unknown.
925#[unsafe(no_mangle)]
926pub extern "C" fn moq_client_set_quic_mtu_discovery(client: u32, enabled: bool) -> i32 {
927 ffi::enter(move || {
928 let client = ffi::parse_id(client)?;
929 State::lock().client.get_mut(client)?.quic.mtu_discovery = Some(enabled);
930 Ok(())
931 })
932}
933
934/// Set the congestion control family.
935///
936/// Either `"loss"` (CUBIC, throughput-oriented) or `"delay"` (BBR, which keeps queues
937/// short and the send rate steady enough for an encoder to track). A NULL or empty value
938/// puts it back to the backend's own default. QUIC only.
939///
940/// Returns zero on success, or a negative code if the handle is unknown or the family is
941/// unrecognized.
942///
943/// # Safety
944/// - The caller must ensure that `family` is either NULL or valid for `family_len` bytes.
945#[unsafe(no_mangle)]
946pub unsafe extern "C" fn moq_client_set_quic_congestion_control(
947 client: u32,
948 family: *const c_char,
949 family_len: usize,
950) -> i32 {
951 ffi::enter(move || {
952 // Parse before taking the lock, so a bad value leaves the config untouched.
953 let family = match unsafe { ffi::parse_str_optional(family, family_len)? } {
954 Some(value) => Some(moq_native::quic::CongestionControl::from_str(value).map_err(Error::InvalidConfig)?),
955 None => None,
956 };
957
958 let client = ffi::parse_id(client)?;
959 State::lock().client.get_mut(client)?.quic.congestion_control = family;
960 Ok(())
961 })
962}
963
964/// Set the directory to write qlog traces into.
965///
966/// A NULL or empty value disables them. Dialing errors if this build has no qlog
967/// support. QUIC only.
968///
969/// Returns zero on success, or a negative code if the handle is unknown.
970///
971/// # Safety
972/// - The caller must ensure that `dir` is either NULL or valid for `dir_len` bytes.
973#[unsafe(no_mangle)]
974pub unsafe extern "C" fn moq_client_set_quic_qlog(client: u32, dir: *const c_char, dir_len: usize) -> i32 {
975 ffi::enter(move || {
976 let dir = unsafe { ffi::parse_str_optional(dir, dir_len)? }.map(Into::into);
977 let client = ffi::parse_id(client)?;
978 State::lock().client.get_mut(client)?.quic.qlog = dir;
979 Ok(())
980 })
981}
982
983/// Read the connect timeout, in milliseconds. See [moq_client_set_connect_timeout].
984///
985/// A knob never set reads back as its default, so a fresh [moq_client_create] handle
986/// reports the defaults a dial would use. That is what a settings UI should show,
987/// rather than repeating numbers that go stale when a default is retuned.
988///
989/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
990///
991/// # Safety
992/// - The caller must ensure that `out` points to a writable `uint64_t`.
993#[unsafe(no_mangle)]
994pub unsafe extern "C" fn moq_client_get_connect_timeout(client: u32, out: *mut u64) -> i32 {
995 ffi::enter(move || {
996 let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
997 let client = ffi::parse_id(client)?;
998 *out = millis(State::lock().client.get_mut(client)?.resolved_connect_timeout());
999 Ok(())
1000 })
1001}
1002
1003/// Read the Happy Eyeballs stagger, in milliseconds. See [moq_client_set_failover_delay]
1004/// and [moq_client_get_connect_timeout] for what an unset knob reports.
1005///
1006/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1007///
1008/// # Safety
1009/// - The caller must ensure that `out` points to a writable `uint64_t`.
1010#[unsafe(no_mangle)]
1011pub unsafe extern "C" fn moq_client_get_failover_delay(client: u32, out: *mut u64) -> i32 {
1012 ffi::enter(move || {
1013 let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1014 let client = ffi::parse_id(client)?;
1015 *out = millis(State::lock().client.get_mut(client)?.resolved_failover_delay());
1016 Ok(())
1017 })
1018}
1019
1020/// Read the first reconnect delay, in milliseconds. See [moq_client_set_backoff_initial].
1021///
1022/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1023///
1024/// # Safety
1025/// - The caller must ensure that `out` points to a writable `uint64_t`.
1026#[unsafe(no_mangle)]
1027pub unsafe extern "C" fn moq_client_get_backoff_initial(client: u32, out: *mut u64) -> i32 {
1028 ffi::enter(move || {
1029 let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1030 let client = ffi::parse_id(client)?;
1031 *out = millis(State::lock().client.get_mut(client)?.backoff.initial);
1032 Ok(())
1033 })
1034}
1035
1036/// Read the reconnect delay multiplier. See [moq_client_set_backoff_multiplier].
1037///
1038/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1039///
1040/// # Safety
1041/// - The caller must ensure that `out` points to a writable `uint32_t`.
1042#[unsafe(no_mangle)]
1043pub unsafe extern "C" fn moq_client_get_backoff_multiplier(client: u32, out: *mut u32) -> i32 {
1044 ffi::enter(move || {
1045 let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1046 let client = ffi::parse_id(client)?;
1047 *out = State::lock().client.get_mut(client)?.backoff.multiplier;
1048 Ok(())
1049 })
1050}
1051
1052/// Read the reconnect delay ceiling, in milliseconds. See [moq_client_set_backoff_max].
1053///
1054/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1055///
1056/// # Safety
1057/// - The caller must ensure that `out` points to a writable `uint64_t`.
1058#[unsafe(no_mangle)]
1059pub unsafe extern "C" fn moq_client_get_backoff_max(client: u32, out: *mut u64) -> i32 {
1060 ffi::enter(move || {
1061 let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1062 let client = ffi::parse_id(client)?;
1063 *out = millis(State::lock().client.get_mut(client)?.backoff.max);
1064 Ok(())
1065 })
1066}
1067
1068/// Read how long reconnecting keeps trying, in milliseconds. Zero means forever. See
1069/// [moq_client_set_backoff_timeout].
1070///
1071/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1072///
1073/// # Safety
1074/// - The caller must ensure that `out` points to a writable `uint64_t`.
1075#[unsafe(no_mangle)]
1076pub unsafe extern "C" fn moq_client_get_backoff_timeout(client: u32, out: *mut u64) -> i32 {
1077 ffi::enter(move || {
1078 let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1079 let client = ffi::parse_id(client)?;
1080 *out = millis(State::lock().client.get_mut(client)?.backoff.timeout);
1081 Ok(())
1082 })
1083}
1084
1085/// Read the maximum concurrent QUIC streams. See [moq_client_set_quic_max_streams].
1086///
1087/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1088///
1089/// # Safety
1090/// - The caller must ensure that `out` points to a writable `uint64_t`.
1091#[unsafe(no_mangle)]
1092pub unsafe extern "C" fn moq_client_get_quic_max_streams(client: u32, out: *mut u64) -> i32 {
1093 ffi::enter(move || {
1094 let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1095 let client = ffi::parse_id(client)?;
1096 *out = State::lock().client.get_mut(client)?.quic.resolve().max_streams;
1097 Ok(())
1098 })
1099}
1100
1101/// Read the QUIC idle timeout, in milliseconds. See [moq_client_set_quic_idle_timeout].
1102///
1103/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1104///
1105/// # Safety
1106/// - The caller must ensure that `out` points to a writable `uint64_t`.
1107#[unsafe(no_mangle)]
1108pub unsafe extern "C" fn moq_client_get_quic_idle_timeout(client: u32, out: *mut u64) -> i32 {
1109 ffi::enter(move || {
1110 let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1111 let client = ffi::parse_id(client)?;
1112 *out = millis(State::lock().client.get_mut(client)?.quic.resolve().idle_timeout);
1113 Ok(())
1114 })
1115}
1116
1117/// Read the QUIC keep-alive interval, in milliseconds. Zero means the pings are
1118/// disabled. See [moq_client_set_quic_keep_alive].
1119///
1120/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1121///
1122/// # Safety
1123/// - The caller must ensure that `out` points to a writable `uint64_t`.
1124#[unsafe(no_mangle)]
1125pub unsafe extern "C" fn moq_client_get_quic_keep_alive(client: u32, out: *mut u64) -> i32 {
1126 ffi::enter(move || {
1127 let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1128 let client = ffi::parse_id(client)?;
1129 let keep_alive = State::lock().client.get_mut(client)?.quic.resolve().keep_alive;
1130 *out = keep_alive.map(millis).unwrap_or(0);
1131 Ok(())
1132 })
1133}
1134
1135/// Read whether the WebSocket fallback races the QUIC attempt. See
1136/// [moq_client_set_websocket_enabled].
1137///
1138/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1139///
1140/// # Safety
1141/// - The caller must ensure that `out` points to a writable `bool`.
1142#[unsafe(no_mangle)]
1143pub unsafe extern "C" fn moq_client_get_websocket_enabled(client: u32, out: *mut bool) -> i32 {
1144 ffi::enter(move || {
1145 let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1146 let client = ffi::parse_id(client)?;
1147 *out = State::lock().client.get_mut(client)?.websocket.enabled;
1148 Ok(())
1149 })
1150}
1151
1152/// Read the WebSocket fallback delay, in milliseconds. See
1153/// [moq_client_set_websocket_delay].
1154///
1155/// Returns zero on success, or a negative code if the handle is unknown or `out` is NULL.
1156///
1157/// # Safety
1158/// - The caller must ensure that `out` points to a writable `uint64_t`.
1159#[unsafe(no_mangle)]
1160pub unsafe extern "C" fn moq_client_get_websocket_delay(client: u32, out: *mut u64) -> i32 {
1161 ffi::enter(move || {
1162 let out = unsafe { out.as_mut() }.ok_or(Error::InvalidPointer)?;
1163 let client = ffi::parse_id(client)?;
1164 let delay = State::lock().client.get_mut(client)?.websocket.delay;
1165 *out = delay.map(millis).unwrap_or(0);
1166 Ok(())
1167 })
1168}
1169
1170/// Start establishing a connection to a MoQ server using a client configuration.
1171///
1172/// Identical to [moq_session_connect] but dials with the settings on `client` (created
1173/// by [moq_client_create]) instead of the defaults. The config is cloned, so the handle
1174/// stays reusable and editable afterwards. A `client` of 0 means the defaults, which is
1175/// exactly what [moq_session_connect] does.
1176///
1177/// Returns a non-zero session handle on success, or a negative code on (immediate)
1178/// failure. Close it with [moq_session_close]. See [moq_session_connect] for the
1179/// `on_status` contract, which is the same here.
1180///
1181/// # Safety
1182/// - The caller must ensure that url is a valid pointer to url_len bytes of data.
1183/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_status` callback.
1184#[unsafe(no_mangle)]
1185pub unsafe extern "C" fn moq_client_connect(
1186 url: *const c_char,
1187 url_len: usize,
1188 client: u32,
1189 origin_publish: u32,
1190 origin_consume: u32,
1191 on_status: Option<extern "C" fn(user_data: *mut c_void, code: i32)>,
1192 user_data: *mut c_void,
1193) -> i32 {
1194 ffi::enter(move || unsafe {
1195 connect_session(
1196 url,
1197 url_len,
1198 client,
1199 origin_publish,
1200 origin_consume,
1201 on_status,
1202 user_data,
1203 )
1204 })
1205}
1206
1207/// Resolve handles under the global lock, prepare the client without it, then insert
1208/// the ready session under a short second lock.
1209unsafe fn connect_session(
1210 url: *const c_char,
1211 url_len: usize,
1212 client: u32,
1213 origin_publish: u32,
1214 origin_consume: u32,
1215 on_status: Option<extern "C" fn(user_data: *mut c_void, code: i32)>,
1216 user_data: *mut c_void,
1217) -> Result<crate::Id, Error> {
1218 let url = ffi::parse_url(url, url_len)?;
1219 let client = ffi::parse_id_optional(client)?;
1220 let origin_publish = ffi::parse_id_optional(origin_publish)?;
1221 let origin_consume = ffi::parse_id_optional(origin_consume)?;
1222
1223 let (config, publish, consume) = {
1224 let state = State::lock();
1225 let config = state.client.config(client)?;
1226 let publish = origin_publish.map(|id| state.origin.get(id)).transpose()?.cloned();
1227 let consume = origin_consume.map(|id| state.origin.get(id)).transpose()?.cloned();
1228 (config, publish, consume)
1229 };
1230
1231 let callback = unsafe { ffi::OnStatus::new(user_data, on_status) };
1232 let request = Connect {
1233 config,
1234 url,
1235 publish,
1236 consume,
1237 callback,
1238 }
1239 .prepare()?;
1240
1241 State::lock().session.connect(request)
1242}
1243
1244/// Start establishing a connection to a MoQ server.
1245///
1246/// Takes origin handles, which are used for publishing and consuming broadcasts respectively.
1247/// - Any broadcasts in `origin_publish` will be announced to the server.
1248/// - Any broadcasts announced by the server will be available in `origin_consume`.
1249/// - If an origin handle is 0, that functionality is completely disabled.
1250///
1251/// This may be called multiple times to connect to different servers.
1252/// Origins can be shared across sessions, useful for fanout or relaying.
1253///
1254/// Dials with the default settings. Use [moq_client_connect] to pin a protocol version,
1255/// adjust TLS trust, or tune the transport.
1256///
1257/// Returns a non-zero handle to the session on success, or a negative code on (immediate) failure.
1258/// You should call [moq_session_close], even on error, to free up resources.
1259///
1260/// The session reconnects automatically with exponential backoff if the connection drops.
1261/// Published broadcasts are re-announced and consumers re-subscribed on each reconnect,
1262/// since the origins outlive the underlying connection.
1263///
1264/// `on_status` reports the session lifecycle through its status code:
1265/// - `> 0` on every (re)connect, carrying the connection epoch (`1` = first connect,
1266/// `2` = first reconnect, and so on), so a reconnect is distinguishable from the
1267/// initial connect. May fire repeatedly. Transient disconnects are not reported.
1268/// - `0` when the session is closed cleanly via [moq_session_close] (terminal).
1269/// - a negative error code if reconnection permanently gives up, e.g. the backoff
1270/// timeout is exceeded (terminal).
1271///
1272/// After a terminal (`<= 0`) status, `on_status` is never called again and `user_data`
1273/// is never touched again, so that final callback is the point to release `user_data`.
1274/// The terminal `0` fires even after [moq_session_close], so do not free `user_data` on
1275/// the close call itself.
1276///
1277/// # Safety
1278/// - The caller must ensure that url is a valid pointer to url_len bytes of data.
1279/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_status` callback.
1280#[unsafe(no_mangle)]
1281pub unsafe extern "C" fn moq_session_connect(
1282 url: *const c_char,
1283 url_len: usize,
1284 origin_publish: u32,
1285 origin_consume: u32,
1286 on_status: Option<extern "C" fn(user_data: *mut c_void, code: i32)>,
1287 user_data: *mut c_void,
1288) -> i32 {
1289 ffi::enter(move || unsafe {
1290 connect_session(url, url_len, 0, origin_publish, origin_consume, on_status, user_data)
1291 })
1292}
1293
1294/// Request that a session shut down.
1295///
1296/// Returns immediately: zero on success, or a negative code if the session is
1297/// unknown or already closing. Does NOT free `user_data`. The
1298/// [moq_session_connect] `on_status` callback still fires once more with a
1299/// terminal `0` (or a negative error), and that final callback is where
1300/// `user_data` should be released. Safe to call from any thread, including from
1301/// within `on_status`.
1302#[unsafe(no_mangle)]
1303pub extern "C" fn moq_session_close(session: u32) -> i32 {
1304 ffi::enter(move || {
1305 let session = ffi::parse_id(session)?;
1306 State::lock().session.close(session)
1307 })
1308}
1309
1310/// Snapshot the current connection statistics for a session.
1311///
1312/// Fills `dst` with a point-in-time view of the underlying QUIC/WebTransport connection
1313/// (RTT, bandwidth estimates, byte/packet counters). Each metric carries a `*_valid` flag
1314/// since availability depends on the transport backend; see [moq_connection_stats].
1315///
1316/// Returns zero on success, or a negative code on failure: the session handle is unknown, or
1317/// the session is currently reconnecting and has no live connection (in which case `dst` is
1318/// left untouched). Safe to call repeatedly to poll stats over the life of the session.
1319///
1320/// # Safety
1321/// - The caller must ensure that `dst` is a valid pointer to a [moq_connection_stats] struct.
1322#[unsafe(no_mangle)]
1323pub unsafe extern "C" fn moq_session_stats(session: u32, dst: *mut moq_connection_stats) -> i32 {
1324 ffi::enter(move || {
1325 let session = ffi::parse_id(session)?;
1326 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1327 let stats = State::lock().session.stats(session)?;
1328 *dst = moq_connection_stats::from(&stats);
1329 Ok(())
1330 })
1331}
1332
1333/// Create an origin for publishing broadcasts.
1334///
1335/// Origins contain any number of broadcasts addressed by path.
1336/// The same broadcast can be published to multiple origins under different paths.
1337///
1338/// [moq_origin_announced] can be used to discover broadcasts published to this origin.
1339/// This is extremely useful for discovering what is available on the server to [moq_origin_request].
1340///
1341/// Returns a non-zero handle to the origin on success.
1342#[unsafe(no_mangle)]
1343pub extern "C" fn moq_origin_create() -> i32 {
1344 ffi::enter(move || State::lock().origin.create())
1345}
1346
1347/// Create a broadcast at `path` on an origin, for publishing media tracks.
1348///
1349/// The broadcast starts live: the origin announces the path so consumers can discover it,
1350/// becoming visible shortly after this returns. Fill it with the `moq_publish_*` functions.
1351/// Toggle discoverability with [moq_publish_set_announce]; [moq_publish_finish] unpublishes
1352/// immediately.
1353///
1354/// Returns a non-zero broadcast handle on success, or a negative code on failure.
1355///
1356/// # Safety
1357/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
1358#[unsafe(no_mangle)]
1359pub unsafe extern "C" fn moq_origin_publish(origin: u32, path: *const c_char, path_len: usize) -> i32 {
1360 ffi::enter(move || {
1361 let origin = ffi::parse_id(origin)?;
1362 let path = unsafe { ffi::parse_str(path, path_len)? };
1363
1364 let mut state = State::lock();
1365 let broadcast = state.origin.publish(origin, path)?;
1366 state.publish.create(broadcast)
1367 })
1368}
1369
1370/// Learn about all broadcasts published to an origin.
1371///
1372/// `on_announce` is invoked with a positive announced ID for each broadcast,
1373/// then exactly once more with a terminal code: `0` (stopped cleanly) or a
1374/// negative error. After the terminal (`<= 0`) callback, `on_announce` is never
1375/// called again and `user_data` is never touched again, so release `user_data`
1376/// there. The terminal callback fires even after [moq_origin_announced_close].
1377///
1378/// - [moq_origin_announced_info] is used to query information about the broadcast.
1379/// - [moq_origin_announced_free] releases each delivered announced ID once read.
1380/// - [moq_origin_announced_close] is used to stop receiving announcements.
1381///
1382/// Returns a non-zero handle on success, or a negative code on failure.
1383///
1384/// # Safety
1385/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_announce` callback.
1386#[unsafe(no_mangle)]
1387pub unsafe extern "C" fn moq_origin_announced(
1388 origin: u32,
1389 on_announce: Option<extern "C" fn(user_data: *mut c_void, announced: i32)>,
1390 user_data: *mut c_void,
1391) -> i32 {
1392 ffi::enter(move || {
1393 let origin = ffi::parse_id(origin)?;
1394 let on_announce = unsafe { ffi::OnStatus::new(user_data, on_announce) };
1395 State::lock().origin.announced(origin, on_announce)
1396 })
1397}
1398
1399/// Query information about a broadcast discovered by [moq_origin_announced].
1400///
1401/// The destination is filled with the broadcast information. The `path` pointer borrows
1402/// the announcement's storage: copy it out before calling [moq_origin_announced_free], which
1403/// invalidates it.
1404///
1405/// Returns a zero on success, or a negative code on failure.
1406///
1407/// # Safety
1408/// - The caller must ensure that `dst` is a valid pointer to a [moq_announced] struct.
1409#[unsafe(no_mangle)]
1410pub unsafe extern "C" fn moq_origin_announced_info(announced: u32, dst: *mut moq_announced) -> i32 {
1411 ffi::enter(move || {
1412 let announced = ffi::parse_id(announced)?;
1413 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1414 State::lock().origin.announced_info(announced, dst)
1415 })
1416}
1417
1418/// Free a single announcement delivered to a [moq_origin_announced] `on_announce` callback.
1419///
1420/// Each announce / unannounce event hands the callback a distinct announcement handle (read
1421/// with [moq_origin_announced_info]); release it here once done to avoid leaking one per event
1422/// over the life of the listener. This is per-announcement and distinct from
1423/// [moq_origin_announced_close], which stops the listener itself. After freeing, any `path`
1424/// pointer obtained from [moq_origin_announced_info] for this handle is dangling.
1425///
1426/// Returns zero on success, or a negative code if the handle is unknown.
1427#[unsafe(no_mangle)]
1428pub extern "C" fn moq_origin_announced_free(announced: u32) -> i32 {
1429 ffi::enter(move || {
1430 let announced = ffi::parse_id(announced)?;
1431 State::lock().origin.announced_free(announced)
1432 })
1433}
1434
1435/// Stop receiving announcements for broadcasts published to an origin.
1436///
1437/// Returns immediately: zero on success, or a negative code if already closed.
1438/// Does NOT free `user_data`. The [moq_origin_announced] `on_announce` callback
1439/// still fires once more with a terminal `0` (or a negative error), and that
1440/// final callback is where `user_data` should be released.
1441#[unsafe(no_mangle)]
1442pub extern "C" fn moq_origin_announced_close(announced: u32) -> i32 {
1443 ffi::enter(move || {
1444 let announced = ffi::parse_id(announced)?;
1445 State::lock().origin.announced_close(announced)
1446 })
1447}
1448
1449/// Consume a broadcast from an origin by path, waiting until it is announced.
1450///
1451/// Resolves against future announcements: it waits for the announcement to arrive (e.g. over the
1452/// network) and then delivers the broadcast handle via `on_broadcast`. Use it right after
1453/// [moq_session_connect] to avoid racing announcement gossip. To resolve against only what is
1454/// announced now (plus any dynamic fallback), use [moq_origin_request] instead.
1455///
1456/// `on_broadcast` is invoked with a positive broadcast handle once announced, then exactly once
1457/// more with a terminal code: `0` (the wait finished, including after
1458/// [moq_origin_consume_announced_close]) or a negative error. After the terminal (`<= 0`) callback,
1459/// `on_broadcast` is never called again and `user_data` is never touched again, so release
1460/// `user_data` there. The broadcast handle is usable with [moq_consume_catalog] / [moq_consume_track]
1461/// and must be freed separately with [moq_consume_close].
1462///
1463/// Returns a non-zero handle to the wait on success, or a negative code on (immediate) failure.
1464///
1465/// # Safety
1466/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
1467/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_broadcast` callback.
1468#[unsafe(no_mangle)]
1469pub unsafe extern "C" fn moq_origin_consume_announced(
1470 origin: u32,
1471 path: *const c_char,
1472 path_len: usize,
1473 on_broadcast: Option<extern "C" fn(user_data: *mut c_void, broadcast: i32)>,
1474 user_data: *mut c_void,
1475) -> i32 {
1476 ffi::enter(move || {
1477 let origin = ffi::parse_id(origin)?;
1478 let path = unsafe { ffi::parse_str(path, path_len)? }.to_string();
1479 let on_broadcast = unsafe { ffi::OnStatus::new(user_data, on_broadcast) };
1480 State::lock().origin.consume_announced(origin, path, on_broadcast)
1481 })
1482}
1483
1484/// Abort a wait started by [moq_origin_consume_announced].
1485///
1486/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
1487/// `user_data`. The [moq_origin_consume_announced] `on_broadcast` callback still fires once more
1488/// with a terminal `0` (or a negative error), and that final callback is where `user_data` should
1489/// be released. Any broadcast handle already delivered is unaffected and must still be freed with
1490/// [moq_consume_close].
1491#[unsafe(no_mangle)]
1492pub extern "C" fn moq_origin_consume_announced_close(task: u32) -> i32 {
1493 ffi::enter(move || {
1494 let task = ffi::parse_id(task)?;
1495 State::lock().origin.consume_announced_close(task)
1496 })
1497}
1498
1499/// Request a broadcast from an origin by path, resolving as soon as it can be served.
1500///
1501/// Resolves against what is announced *now* plus any dynamic fallback, where
1502/// [moq_origin_consume_announced] waits indefinitely for a future announcement: it returns an
1503/// already-announced broadcast at once, otherwise falls back to a dynamic handler on the origin
1504/// (if any), and fails when neither can serve the path. It does NOT wait for a later
1505/// announcement.
1506///
1507/// `on_broadcast` is invoked with a positive broadcast handle once served, then exactly once more
1508/// with a terminal code: `0` (finished, including after [moq_origin_request_close]) or a negative
1509/// error. After the terminal (`<= 0`) callback, `user_data` is never touched again, so release it
1510/// there. The broadcast handle is usable with [moq_consume_catalog] / [moq_consume_track] and must
1511/// be freed separately with [moq_consume_close].
1512///
1513/// Returns a non-zero handle to the request on success, or a negative code on (immediate) failure.
1514///
1515/// # Safety
1516/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
1517/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_broadcast` callback.
1518#[unsafe(no_mangle)]
1519pub unsafe extern "C" fn moq_origin_request(
1520 origin: u32,
1521 path: *const c_char,
1522 path_len: usize,
1523 on_broadcast: Option<extern "C" fn(user_data: *mut c_void, broadcast: i32)>,
1524 user_data: *mut c_void,
1525) -> i32 {
1526 ffi::enter(move || {
1527 let origin = ffi::parse_id(origin)?;
1528 let path = unsafe { ffi::parse_str(path, path_len)? }.to_string();
1529 let on_broadcast = unsafe { ffi::OnStatus::new(user_data, on_broadcast) };
1530 State::lock().origin.request(origin, path, on_broadcast)
1531 })
1532}
1533
1534/// Abort a request started by [moq_origin_request].
1535///
1536/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
1537/// `user_data`; the [moq_origin_request] `on_broadcast` callback fires once more with a terminal
1538/// code, which is where `user_data` should be released. Any broadcast handle already delivered is
1539/// unaffected and must still be freed with [moq_consume_close].
1540#[unsafe(no_mangle)]
1541pub extern "C" fn moq_origin_request_close(task: u32) -> i32 {
1542 ffi::enter(move || {
1543 let task = ffi::parse_id(task)?;
1544 State::lock().origin.consume_announced_close(task)
1545 })
1546}
1547
1548/// Close an origin and clean up its resources.
1549///
1550/// Returns a zero on success, or a negative code on failure.
1551#[unsafe(no_mangle)]
1552pub extern "C" fn moq_origin_close(origin: u32) -> i32 {
1553 ffi::enter(move || {
1554 let origin = ffi::parse_id(origin)?;
1555 State::lock().origin.close(origin)
1556 })
1557}
1558
1559/// Set whether a broadcast created by [moq_origin_publish] is live: announced by its origin.
1560///
1561/// A non-live broadcast stays reachable by exact path for subscribes and fetches; it just is
1562/// not announced. This is how a publisher goes on and off the air without tearing down the
1563/// broadcast.
1564///
1565/// Returns a zero on success, or a negative code on failure.
1566#[unsafe(no_mangle)]
1567pub extern "C" fn moq_publish_set_announce(broadcast: u32, announce: bool) -> i32 {
1568 ffi::enter(move || {
1569 let broadcast = ffi::parse_id(broadcast)?;
1570 State::lock().publish.set_announce(broadcast, announce)
1571 })
1572}
1573
1574/// Finish a broadcast and release it, ending its catalog cleanly.
1575///
1576/// Subscribers see a normal end of stream rather than an error, and the origin unpublishes
1577/// the path immediately.
1578///
1579/// Returns a zero on success, or a negative code on failure.
1580#[unsafe(no_mangle)]
1581pub extern "C" fn moq_publish_finish(broadcast: u32) -> i32 {
1582 ffi::enter(move || {
1583 let broadcast = ffi::parse_id(broadcast)?;
1584 State::lock().publish.finish(broadcast)
1585 })
1586}
1587
1588/// Create a new media track for a broadcast
1589///
1590/// All frames in [moq_publish_media_frame] must be written in decode order.
1591/// The `format` controls the encoding, both of `init` and frame payloads.
1592///
1593/// Returns a non-zero handle to the track on success, or a negative code on failure.
1594///
1595/// # Safety
1596/// - The caller must ensure that format is a valid pointer to format_len bytes of data.
1597/// - The caller must ensure that init is a valid pointer to init_size bytes of data.
1598#[unsafe(no_mangle)]
1599pub unsafe extern "C" fn moq_publish_media(
1600 broadcast: u32,
1601 format: *const c_char,
1602 format_len: usize,
1603 init: *const u8,
1604 init_size: usize,
1605) -> i32 {
1606 ffi::enter(move || {
1607 let broadcast = ffi::parse_id(broadcast)?;
1608 let format = unsafe { ffi::parse_str(format, format_len)? };
1609 let init = unsafe { ffi::parse_slice(init, init_size)? };
1610
1611 State::lock().publish.media(broadcast, format, init)
1612 })
1613}
1614
1615/// Finish a media track, flushing any buffered frames. No more frames can be written.
1616///
1617/// Returns a zero on success, or a negative code on failure.
1618#[unsafe(no_mangle)]
1619pub extern "C" fn moq_publish_media_finish(export: u32) -> i32 {
1620 ffi::enter(move || {
1621 let export = ffi::parse_id(export)?;
1622 State::lock().publish.media_finish(export)
1623 })
1624}
1625
1626/// Write data to a track.
1627///
1628/// The encoding of `data` depends on the track `format`.
1629/// The timestamp is in microseconds.
1630///
1631/// Returns a zero on success, or a negative code on failure.
1632///
1633/// # Safety
1634/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
1635#[unsafe(no_mangle)]
1636pub unsafe extern "C" fn moq_publish_media_frame(
1637 media: u32,
1638 payload: *const u8,
1639 payload_size: usize,
1640 timestamp_us: u64,
1641) -> i32 {
1642 ffi::enter(move || {
1643 let media = ffi::parse_id(media)?;
1644 let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
1645 let timestamp = hang::container::Timestamp::from_micros(timestamp_us)?;
1646 State::lock().publish.media_frame(media, payload, timestamp)
1647 })
1648}
1649
1650/// Replace the catalog properties shared by every video rendition.
1651///
1652/// 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.
1653///
1654/// Returns zero on success, or a negative code on failure.
1655///
1656/// # Safety
1657/// - The caller must ensure that `properties` points to a valid [moq_video_properties].
1658#[unsafe(no_mangle)]
1659pub unsafe extern "C" fn moq_publish_video_properties(broadcast: u32, properties: *const moq_video_properties) -> i32 {
1660 ffi::enter(move || {
1661 let broadcast = ffi::parse_id(broadcast)?;
1662 let properties = unsafe { properties.as_ref() }.ok_or(Error::InvalidPointer)?;
1663
1664 let mut value = hang::catalog::VideoProperties::default();
1665 value.display = properties.has_display.then_some(hang::catalog::Display {
1666 width: properties.display_width,
1667 height: properties.display_height,
1668 });
1669 value.rotation = properties.has_rotation.then_some(properties.rotation);
1670 value.flip = properties.has_flip.then_some(properties.flip);
1671
1672 State::lock().publish.video_properties(broadcast, value)
1673 })
1674}
1675
1676/// Add or replace a video rendition in a broadcast's catalog.
1677///
1678/// This is the producer counterpart to [moq_consume_video_config]: instead of
1679/// reading a rendition out of a catalog, it writes one into the catalog of a
1680/// broadcast created with [moq_origin_publish]. The rendition is keyed by
1681/// `config.name`; calling this again with the same name replaces it. The
1682/// updated catalog is published to subscribers automatically.
1683///
1684/// The struct fields are read as inputs:
1685/// - `name` / `codec` are required (NOT NULL terminated) string slices.
1686/// - `description` may be NULL to omit it.
1687/// - `coded_width` / `coded_height` may be NULL to omit them.
1688///
1689/// Returns a zero on success, or a negative code on failure.
1690///
1691/// # Safety
1692/// - The caller must ensure that `config` points to a valid [moq_video_config].
1693/// - The caller must ensure each non-NULL pointer inside `config` is valid for its length.
1694#[unsafe(no_mangle)]
1695pub unsafe extern "C" fn moq_publish_video_config(broadcast: u32, config: *const moq_video_config) -> i32 {
1696 ffi::enter(move || {
1697 let broadcast = ffi::parse_id(broadcast)?;
1698 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1699
1700 let name = unsafe { ffi::parse_str(config.name, config.name_len)? };
1701 let codec = unsafe { ffi::parse_str(config.codec, config.codec_len)? };
1702 let codec = hang::catalog::VideoCodec::from_str(codec).map_err(Error::Hang)?;
1703
1704 let mut video = hang::catalog::VideoConfig::new(codec);
1705 if !config.description.is_null() {
1706 let description = unsafe { ffi::parse_slice(config.description, config.description_len)? };
1707 video.description = Some(bytes::Bytes::copy_from_slice(description));
1708 }
1709 video.coded_width = unsafe { config.coded_width.as_ref() }.copied();
1710 video.coded_height = unsafe { config.coded_height.as_ref() }.copied();
1711
1712 State::lock().publish.video_config(broadcast, name, video)
1713 })
1714}
1715
1716/// Add or replace an audio rendition in a broadcast's catalog.
1717///
1718/// This is the producer counterpart to [moq_consume_audio_config]. The rendition
1719/// is keyed by `config.name`; calling this again with the same name replaces it.
1720/// The updated catalog is published to subscribers automatically.
1721///
1722/// The struct fields are read as inputs:
1723/// - `name` / `codec` are required (NOT NULL terminated) string slices.
1724/// - `sample_rate` / `channel_count` are required.
1725/// - `description` may be NULL to omit it.
1726///
1727/// Returns a zero on success, or a negative code on failure.
1728///
1729/// # Safety
1730/// - The caller must ensure that `config` points to a valid [moq_audio_config].
1731/// - The caller must ensure each non-NULL pointer inside `config` is valid for its length.
1732#[unsafe(no_mangle)]
1733pub unsafe extern "C" fn moq_publish_audio_config(broadcast: u32, config: *const moq_audio_config) -> i32 {
1734 ffi::enter(move || {
1735 let broadcast = ffi::parse_id(broadcast)?;
1736 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1737
1738 let name = unsafe { ffi::parse_str(config.name, config.name_len)? };
1739 let codec = unsafe { ffi::parse_str(config.codec, config.codec_len)? };
1740 let codec = hang::catalog::AudioCodec::from_str(codec).map_err(Error::Hang)?;
1741
1742 let mut audio = hang::catalog::AudioConfig::new(codec, config.sample_rate, config.channel_count);
1743 if !config.description.is_null() {
1744 let description = unsafe { ffi::parse_slice(config.description, config.description_len)? };
1745 audio.description = Some(bytes::Bytes::copy_from_slice(description));
1746 }
1747
1748 State::lock().publish.audio_config(broadcast, name, audio)
1749 })
1750}
1751
1752/// Remove a video rendition from a broadcast's catalog by name.
1753///
1754/// This is a no-op if no rendition with that name exists. The updated catalog is
1755/// published to subscribers automatically.
1756///
1757/// Returns a zero on success, or a negative code on failure.
1758///
1759/// # Safety
1760/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
1761#[unsafe(no_mangle)]
1762pub unsafe extern "C" fn moq_publish_video_remove(broadcast: u32, name: *const c_char, name_len: usize) -> i32 {
1763 ffi::enter(move || {
1764 let broadcast = ffi::parse_id(broadcast)?;
1765 let name = unsafe { ffi::parse_str(name, name_len)? };
1766 State::lock().publish.video_remove(broadcast, name)
1767 })
1768}
1769
1770/// Remove an audio rendition from a broadcast's catalog by name.
1771///
1772/// This is a no-op if no rendition with that name exists. The updated catalog is
1773/// published to subscribers automatically.
1774///
1775/// Returns a zero on success, or a negative code on failure.
1776///
1777/// # Safety
1778/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
1779#[unsafe(no_mangle)]
1780pub unsafe extern "C" fn moq_publish_audio_remove(broadcast: u32, name: *const c_char, name_len: usize) -> i32 {
1781 ffi::enter(move || {
1782 let broadcast = ffi::parse_id(broadcast)?;
1783 let name = unsafe { ffi::parse_str(name, name_len)? };
1784 State::lock().publish.audio_remove(broadcast, name)
1785 })
1786}
1787
1788/// Set (or replace) a top-level application catalog section by name.
1789///
1790/// This is the producer counterpart to [moq_consume_catalog_section] /
1791/// [moq_consume_catalog_section_at]: it writes an arbitrary top-level JSON key into the
1792/// catalog of a broadcast created with [moq_origin_publish], beyond the
1793/// `video`/`audio` keys owned by the media pipeline. Calling it again with the
1794/// same name replaces the section. The updated catalog is published to
1795/// subscribers automatically.
1796///
1797/// `json` is a JSON document (object, array, string, ...) as `json_len` bytes of
1798/// UTF-8. Returns a zero on success, or a negative code on failure: invalid JSON
1799/// yields a Json error (-37); a reserved `name` (`video`/`audio`) yields a mux error.
1800///
1801/// # Safety
1802/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
1803/// - The caller must ensure that json is a valid pointer to json_len bytes of data.
1804#[unsafe(no_mangle)]
1805pub unsafe extern "C" fn moq_publish_catalog_section(
1806 broadcast: u32,
1807 name: *const c_char,
1808 name_len: usize,
1809 json: *const c_char,
1810 json_len: usize,
1811) -> i32 {
1812 ffi::enter(move || {
1813 let broadcast = ffi::parse_id(broadcast)?;
1814 let name = unsafe { ffi::parse_str(name, name_len)? };
1815 let json = unsafe { ffi::parse_str(json, json_len)? };
1816 let value: serde_json::Value = serde_json::from_str(json)?;
1817 State::lock().publish.catalog_section_set(broadcast, name, value)
1818 })
1819}
1820
1821/// Remove a top-level application catalog section by name.
1822///
1823/// This is a no-op if no section with that name exists. The updated catalog is
1824/// published to subscribers automatically.
1825///
1826/// Returns a zero on success, or a negative code on failure.
1827///
1828/// # Safety
1829/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
1830#[unsafe(no_mangle)]
1831pub unsafe extern "C" fn moq_publish_catalog_section_remove(
1832 broadcast: u32,
1833 name: *const c_char,
1834 name_len: usize,
1835) -> i32 {
1836 ffi::enter(move || {
1837 let broadcast = ffi::parse_id(broadcast)?;
1838 let name = unsafe { ffi::parse_str(name, name_len)? };
1839 State::lock().publish.catalog_section_remove(broadcast, name)
1840 })
1841}
1842
1843/// Create a raw track on a broadcast for arbitrary byte payloads.
1844///
1845/// Unlike [moq_publish_media], this is the bare moq-net primitive: no
1846/// codec, container, or catalog framing. Frames written to it are delivered
1847/// as-is to subscribers using [moq_consume_track]. Use it for non-media tracks
1848/// (control channels, JSON metadata, etc.), or pair it with
1849/// [moq_publish_video_config] / [moq_publish_audio_config] to also describe the
1850/// track in the catalog. Pass NULL for `info` to use moq-net defaults.
1851///
1852/// Returns a non-zero handle to the track on success, or a negative code on failure.
1853///
1854/// # Safety
1855/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
1856/// - The caller must ensure that info is either NULL or a valid pointer to a [moq_track_info] struct.
1857#[unsafe(no_mangle)]
1858pub unsafe extern "C" fn moq_publish_track(
1859 broadcast: u32,
1860 name: *const c_char,
1861 name_len: usize,
1862 info: *const moq_track_info,
1863) -> i32 {
1864 ffi::enter(move || {
1865 let broadcast = ffi::parse_id(broadcast)?;
1866 let name = unsafe { ffi::parse_str(name, name_len)? };
1867 // Default raw tracks to a microsecond timescale even when no info is given.
1868 let info = match unsafe { info.as_ref() } {
1869 Some(info) => moq_net::track::Info::try_from(info)?,
1870 None => moq_net::track::Info::default().with_timescale(moq_net::Timescale::MICRO),
1871 };
1872 State::lock().publish.track(broadcast, name, Some(info))
1873 })
1874}
1875
1876/// Append a new group to a raw track, returning a group producer.
1877///
1878/// Groups are delivered independently and each may contain any number of frames
1879/// written via [moq_publish_group_frame]. Sequence numbers auto-increment.
1880///
1881/// Returns a non-zero handle to the group on success, or a negative code on failure.
1882#[unsafe(no_mangle)]
1883pub extern "C" fn moq_publish_track_group(track: u32) -> i32 {
1884 ffi::enter(move || {
1885 let track = ffi::parse_id(track)?;
1886 State::lock().publish.track_group(track)
1887 })
1888}
1889
1890/// Create a raw group with an explicit sequence number.
1891///
1892/// Returns a non-zero group handle on success, or a negative code on failure.
1893#[unsafe(no_mangle)]
1894pub extern "C" fn moq_publish_track_group_at(track: u32, sequence: u64) -> i32 {
1895 ffi::enter(move || {
1896 let track = ffi::parse_id(track)?;
1897 State::lock().publish.track_group_at(track, sequence)
1898 })
1899}
1900
1901/// Write a single-frame group to a raw track with a timestamp.
1902///
1903/// Convenience for the common one-frame-per-group pattern. Equivalent to
1904/// appending a group, writing one frame, and finishing it.
1905/// The timestamp is in microseconds.
1906///
1907/// Returns a zero on success, or a negative code on failure.
1908///
1909/// # Safety
1910/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
1911#[unsafe(no_mangle)]
1912pub unsafe extern "C" fn moq_publish_track_frame(
1913 track: u32,
1914 payload: *const u8,
1915 payload_size: usize,
1916 timestamp_us: u64,
1917) -> i32 {
1918 ffi::enter(move || {
1919 let track = ffi::parse_id(track)?;
1920 let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
1921 let timestamp = moq_net::Timestamp::from_micros(timestamp_us)?;
1922 State::lock().publish.track_frame(track, timestamp, payload)
1923 })
1924}
1925
1926/// Send a best-effort datagram on a raw track created by [moq_publish_track].
1927///
1928/// Takes `payload` then `timestamp_us`, matching [moq_publish_track_frame]. The payload must
1929/// be at most 1200 bytes. On success the datagram's per-track sequence number (shared with the
1930/// group namespace) is written to `out_sequence` when it is non-NULL. Datagrams are
1931/// delivered only on transports and wire versions with a datagram channel; there is no
1932/// group fallback.
1933///
1934/// Returns a zero on success, or a negative code on failure.
1935///
1936/// # Safety
1937/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
1938/// - `out_sequence` must be NULL or a valid pointer to a `uint64_t`.
1939#[unsafe(no_mangle)]
1940pub unsafe extern "C" fn moq_publish_track_datagram(
1941 track: u32,
1942 payload: *const u8,
1943 payload_size: usize,
1944 timestamp_us: u64,
1945 out_sequence: *mut u64,
1946) -> i32 {
1947 ffi::enter(move || {
1948 let track = ffi::parse_id(track)?;
1949 let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
1950 let sequence = State::lock().publish.track_datagram(track, timestamp_us, payload)?;
1951 if let Some(out) = unsafe { out_sequence.as_mut() } {
1952 *out = sequence;
1953 }
1954 Ok(())
1955 })
1956}
1957
1958/// Finish a raw track. No more groups or frames can be written.
1959///
1960/// Returns a zero on success, or a negative code on failure.
1961#[unsafe(no_mangle)]
1962pub extern "C" fn moq_publish_track_finish(track: u32) -> i32 {
1963 ffi::enter(move || {
1964 let track = ffi::parse_id(track)?;
1965 State::lock().publish.track_finish(track)
1966 })
1967}
1968
1969/// Declare a raw track's exclusive final group sequence.
1970///
1971/// Groups below `final_sequence` may still be created. Groups at or above it
1972/// are rejected. The track remains open for groups below the boundary. Call
1973/// [moq_publish_track_finish] after producing the remaining groups.
1974#[unsafe(no_mangle)]
1975pub extern "C" fn moq_publish_track_finish_at(track: u32, final_sequence: u64) -> i32 {
1976 ffi::enter(move || {
1977 let track = ffi::parse_id(track)?;
1978 State::lock().publish.track_finish_at(track, final_sequence)
1979 })
1980}
1981
1982/// Abort a raw track with an application error code.
1983#[unsafe(no_mangle)]
1984pub extern "C" fn moq_publish_track_abort(track: u32, error_code: u16) -> i32 {
1985 ffi::enter(move || {
1986 let track = ffi::parse_id(track)?;
1987 State::lock().publish.track_abort(track, error_code)
1988 })
1989}
1990
1991/// Write a frame into a raw group created by [moq_publish_track_group].
1992///
1993/// The timestamp is in microseconds.
1994///
1995/// Returns a zero on success, or a negative code on failure.
1996///
1997/// # Safety
1998/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
1999#[unsafe(no_mangle)]
2000pub unsafe extern "C" fn moq_publish_group_frame(
2001 group: u32,
2002 payload: *const u8,
2003 payload_size: usize,
2004 timestamp_us: u64,
2005) -> i32 {
2006 ffi::enter(move || {
2007 let group = ffi::parse_id(group)?;
2008 let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
2009 let timestamp = moq_net::Timestamp::from_micros(timestamp_us)?;
2010 State::lock().publish.group_frame(group, timestamp, payload)
2011 })
2012}
2013
2014/// Finish a raw group. No more frames can be written.
2015///
2016/// Returns a zero on success, or a negative code on failure.
2017#[unsafe(no_mangle)]
2018pub extern "C" fn moq_publish_group_finish(group: u32) -> i32 {
2019 ffi::enter(move || {
2020 let group = ffi::parse_id(group)?;
2021 State::lock().publish.group_finish(group)
2022 })
2023}
2024
2025/// Abort a raw group with an application error code.
2026#[unsafe(no_mangle)]
2027pub extern "C" fn moq_publish_group_abort(group: u32, error_code: u16) -> i32 {
2028 ffi::enter(move || {
2029 let group = ffi::parse_id(group)?;
2030 State::lock().publish.group_abort(group, error_code)
2031 })
2032}
2033
2034/// Create a JSON snapshot track (lossy latest-value) on a broadcast.
2035///
2036/// Values published via [moq_publish_json_snapshot_update] reach subscribers as a single latest
2037/// state; a late joiner only sees the newest. Advertise the track in the catalog with
2038/// [moq_publish_catalog_section] if consumers should discover it.
2039///
2040/// Returns a non-zero handle to the JSON producer on success, or a negative code on failure.
2041///
2042/// # Safety
2043/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
2044#[unsafe(no_mangle)]
2045pub unsafe extern "C" fn moq_publish_json_snapshot(
2046 broadcast: u32,
2047 name: *const c_char,
2048 name_len: usize,
2049 config: *const moq_json_snapshot_config,
2050) -> i32 {
2051 ffi::enter(move || {
2052 let broadcast = ffi::parse_id(broadcast)?;
2053 let name = unsafe { ffi::parse_str(name, name_len)? };
2054 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
2055 let mut producer = moq_json::snapshot::ProducerConfig::default();
2056 producer.delta_ratio = config.delta_ratio;
2057 producer.compression = config.compression;
2058 State::lock().publish.json_snapshot(broadcast, name, producer)
2059 })
2060}
2061
2062/// Publish a new value to a JSON snapshot track. `value` is a UTF-8 JSON document. A no-op if
2063/// unchanged from the previous update.
2064///
2065/// Returns a zero on success, or a negative code on failure.
2066///
2067/// # Safety
2068/// - The caller must ensure `value` is a valid pointer to `value_len` bytes.
2069#[unsafe(no_mangle)]
2070pub unsafe extern "C" fn moq_publish_json_snapshot_update(json: u32, value: *const c_char, value_len: usize) -> i32 {
2071 ffi::enter(move || {
2072 let json = ffi::parse_id(json)?;
2073 let value = unsafe { ffi::parse_slice(value.cast::<u8>(), value_len)? };
2074 let value = serde_json::from_slice(value)?;
2075 State::lock().publish.json_snapshot_update(json, value)
2076 })
2077}
2078
2079/// Finish a JSON snapshot track. No more values can be published.
2080///
2081/// Returns a zero on success, or a negative code on failure.
2082#[unsafe(no_mangle)]
2083pub extern "C" fn moq_publish_json_snapshot_finish(json: u32) -> i32 {
2084 ffi::enter(move || {
2085 let json = ffi::parse_id(json)?;
2086 State::lock().publish.json_snapshot_finish(json)
2087 })
2088}
2089
2090/// Create a JSON stream track (lossless append-log) on a broadcast.
2091///
2092/// Every record appended via [moq_publish_json_stream_append] is preserved and delivered in order.
2093///
2094/// Returns a non-zero handle to the JSON stream producer on success, or a negative code on failure.
2095///
2096/// # Safety
2097/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
2098#[unsafe(no_mangle)]
2099pub unsafe extern "C" fn moq_publish_json_stream(
2100 broadcast: u32,
2101 name: *const c_char,
2102 name_len: usize,
2103 config: *const moq_json_stream_config,
2104) -> i32 {
2105 ffi::enter(move || {
2106 let broadcast = ffi::parse_id(broadcast)?;
2107 let name = unsafe { ffi::parse_str(name, name_len)? };
2108 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
2109 let producer = moq_json::stream::ProducerConfig::default().with_compression(config.compression);
2110 State::lock().publish.json_stream(broadcast, name, producer)
2111 })
2112}
2113
2114/// Append one record to a JSON stream track. `value` is a UTF-8 JSON document.
2115///
2116/// Returns a zero on success, or a negative code on failure.
2117///
2118/// # Safety
2119/// - The caller must ensure `value` is a valid pointer to `value_len` bytes.
2120#[unsafe(no_mangle)]
2121pub unsafe extern "C" fn moq_publish_json_stream_append(stream: u32, value: *const c_char, value_len: usize) -> i32 {
2122 ffi::enter(move || {
2123 let stream = ffi::parse_id(stream)?;
2124 let value = unsafe { ffi::parse_slice(value.cast::<u8>(), value_len)? };
2125 let value = serde_json::from_slice(value)?;
2126 State::lock().publish.json_stream_append(stream, value)
2127 })
2128}
2129
2130/// Finish a JSON stream track. No more records can be appended.
2131///
2132/// Returns a zero on success, or a negative code on failure.
2133#[unsafe(no_mangle)]
2134pub extern "C" fn moq_publish_json_stream_finish(stream: u32) -> i32 {
2135 ffi::enter(move || {
2136 let stream = ffi::parse_id(stream)?;
2137 State::lock().publish.json_stream_finish(stream)
2138 })
2139}
2140
2141/// Create a catalog consumer for a broadcast.
2142///
2143/// `on_catalog` is invoked with a positive catalog ID for each catalog update
2144/// (usable to query video/audio track information), then exactly once more with
2145/// a terminal code: `0` (closed cleanly) or a negative error. After the terminal
2146/// (`<= 0`) callback, `on_catalog` is never called again and `user_data` is never
2147/// touched again, so release `user_data` there. The terminal callback fires even
2148/// after [moq_consume_catalog_close].
2149///
2150/// Returns a non-zero handle on success, or a negative code on failure.
2151///
2152/// # Safety
2153/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_catalog` callback.
2154#[unsafe(no_mangle)]
2155pub unsafe extern "C" fn moq_consume_catalog(
2156 broadcast: u32,
2157 on_catalog: Option<extern "C" fn(user_data: *mut c_void, catalog: i32)>,
2158 user_data: *mut c_void,
2159) -> i32 {
2160 ffi::enter(move || {
2161 let broadcast = ffi::parse_id(broadcast)?;
2162 let on_catalog = unsafe { ffi::OnStatus::new(user_data, on_catalog) };
2163 State::lock().consume.catalog(broadcast, on_catalog)
2164 })
2165}
2166
2167/// Stop a catalog consumer's background subscription.
2168///
2169/// Returns immediately: zero on success, or a negative code if already closed.
2170/// Does NOT free `user_data`; the [moq_consume_catalog] callback still fires once
2171/// more with a terminal `0` (or a negative error), which is where `user_data`
2172/// should be released. Catalog snapshots previously delivered via the callback
2173/// remain valid until freed with [moq_consume_catalog_free].
2174#[unsafe(no_mangle)]
2175pub extern "C" fn moq_consume_catalog_close(catalog: u32) -> i32 {
2176 ffi::enter(move || {
2177 let catalog = ffi::parse_id(catalog)?;
2178 State::lock().consume.catalog_close(catalog)
2179 })
2180}
2181
2182/// Free a catalog snapshot received via the [moq_consume_catalog] callback.
2183///
2184/// This releases the snapshot and invalidates any borrowed references (e.g. pointers
2185/// returned by [moq_consume_video_config] or [moq_consume_audio_config]).
2186///
2187/// Returns a zero on success, or a negative code on failure.
2188#[unsafe(no_mangle)]
2189pub extern "C" fn moq_consume_catalog_free(catalog: u32) -> i32 {
2190 ffi::enter(move || {
2191 let catalog = ffi::parse_id(catalog)?;
2192 State::lock().consume.catalog_free(catalog)
2193 })
2194}
2195
2196/// Query information about a video track in a catalog.
2197///
2198/// The destination is filled with the video track information.
2199///
2200/// Returns a zero on success, or a negative code on failure.
2201///
2202/// # Safety
2203/// - The caller must ensure that `dst` is a valid pointer to a [moq_video_config] struct.
2204/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
2205#[unsafe(no_mangle)]
2206pub unsafe extern "C" fn moq_consume_video_config(catalog: u32, index: u32, dst: *mut moq_video_config) -> i32 {
2207 ffi::enter(move || {
2208 let catalog = ffi::parse_id(catalog)?;
2209 let index = index as usize;
2210 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2211 State::lock().consume.video_config(catalog, index, dst)
2212 })
2213}
2214
2215/// Query the catalog properties shared by every video rendition.
2216///
2217/// The destination is filled by value and remains valid after the catalog snapshot is freed.
2218/// Inspect each `has_*` flag before reading its value.
2219///
2220/// Returns zero on success, or a negative code on failure.
2221///
2222/// # Safety
2223/// - The caller must ensure that `dst` points to a valid [moq_video_properties].
2224#[unsafe(no_mangle)]
2225pub unsafe extern "C" fn moq_consume_video_properties(catalog: u32, dst: *mut moq_video_properties) -> i32 {
2226 ffi::enter(move || {
2227 let catalog = ffi::parse_id(catalog)?;
2228 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2229 State::lock().consume.video_properties(catalog, dst)
2230 })
2231}
2232
2233/// Query information about an audio track in a catalog.
2234///
2235/// The destination is filled with the audio track information.
2236///
2237/// Returns a zero on success, or a negative code on failure.
2238///
2239/// # Safety
2240/// - The caller must ensure that `dst` is a valid pointer to a [moq_audio_config] struct.
2241/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
2242#[unsafe(no_mangle)]
2243pub unsafe extern "C" fn moq_consume_audio_config(catalog: u32, index: u32, dst: *mut moq_audio_config) -> i32 {
2244 ffi::enter(move || {
2245 let catalog = ffi::parse_id(catalog)?;
2246 let index = index as usize;
2247 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2248 State::lock().consume.audio_config(catalog, index, dst)
2249 })
2250}
2251
2252/// Number of untyped application catalog sections in a catalog snapshot.
2253///
2254/// These are the top-level catalog keys beyond `video`/`audio`, carried through
2255/// verbatim. Iterate them by index with [moq_consume_catalog_section_at], or look one up
2256/// directly by name with [moq_consume_catalog_section].
2257///
2258/// Returns the count (>= 0) on success, or a negative code on failure.
2259#[unsafe(no_mangle)]
2260pub extern "C" fn moq_consume_catalog_section_count(catalog: u32) -> i32 {
2261 ffi::enter(move || {
2262 let catalog = ffi::parse_id(catalog)?;
2263 State::lock().consume.catalog_section_count(catalog)
2264 })
2265}
2266
2267/// Query an application catalog section by index, keyed by name.
2268///
2269/// Fills `dst` with the section's name and JSON value at `index`, in the range
2270/// `[0, moq_consume_catalog_section_count)`. Both pointers borrow the snapshot's storage
2271/// and stay valid until it is freed with [moq_consume_catalog_free].
2272///
2273/// Returns a zero on success, or a negative code on failure (e.g. `index` out of
2274/// range).
2275///
2276/// # Safety
2277/// - The caller must ensure that `dst` is a valid pointer to a [moq_section] struct.
2278/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
2279#[unsafe(no_mangle)]
2280pub unsafe extern "C" fn moq_consume_catalog_section_at(catalog: u32, index: u32, dst: *mut moq_section) -> i32 {
2281 ffi::enter(move || {
2282 let catalog = ffi::parse_id(catalog)?;
2283 let index = index as usize;
2284 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2285 State::lock().consume.catalog_section_at(catalog, index, dst)
2286 })
2287}
2288
2289/// Look up an application catalog section by name.
2290///
2291/// Fills `dst` with the section's JSON value (the document to parse yourself).
2292/// The pointer borrows the snapshot's storage and stays valid until it is freed
2293/// with [moq_consume_catalog_free].
2294///
2295/// Returns a zero on success, or a negative code on failure: no section with that
2296/// name yields a not-found error.
2297///
2298/// # Safety
2299/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
2300/// - The caller must ensure that `dst` is a valid pointer to a [moq_string] struct.
2301/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
2302#[unsafe(no_mangle)]
2303pub unsafe extern "C" fn moq_consume_catalog_section(
2304 catalog: u32,
2305 name: *const c_char,
2306 name_len: usize,
2307 dst: *mut moq_string,
2308) -> i32 {
2309 ffi::enter(move || {
2310 let catalog = ffi::parse_id(catalog)?;
2311 let name = unsafe { ffi::parse_str(name, name_len)? };
2312 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2313 State::lock().consume.catalog_section_get(catalog, name, dst)
2314 })
2315}
2316
2317/// Consume a video track from a broadcast, delivering frames in order.
2318///
2319/// - `max_latency_ms` controls the maximum amount of buffering allowed before skipping a GoP.
2320/// - `on_frame` is called with a positive frame ID per frame, then exactly once
2321/// more with a terminal code: `0` (closed cleanly) or a negative error. After
2322/// the terminal (`<= 0`) callback, `on_frame` is never called again and
2323/// `user_data` is never touched again, so release `user_data` there. The
2324/// terminal callback fires even after [moq_consume_video_close].
2325///
2326/// Returns a non-zero handle to the track on success, or a negative code on failure.
2327///
2328/// # Safety
2329/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
2330#[unsafe(no_mangle)]
2331pub unsafe extern "C" fn moq_consume_video(
2332 catalog: u32,
2333 index: u32,
2334 max_latency_ms: u64,
2335 on_frame: Option<extern "C" fn(user_data: *mut c_void, frame: i32)>,
2336 user_data: *mut c_void,
2337) -> i32 {
2338 ffi::enter(move || {
2339 let catalog = ffi::parse_id(catalog)?;
2340 let index = index as usize;
2341 let max_latency = std::time::Duration::from_millis(max_latency_ms);
2342 let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame) };
2343 State::lock().consume.video(catalog, index, max_latency, on_frame)
2344 })
2345}
2346
2347/// Stop a video track consumer's background task.
2348///
2349/// Returns immediately: zero on success, or a negative code if already closed.
2350/// Does NOT free `user_data`; the [moq_consume_video] `on_frame` callback
2351/// still fires once more with a terminal `0` (or a negative error), which is
2352/// where `user_data` should be released.
2353#[unsafe(no_mangle)]
2354pub extern "C" fn moq_consume_video_close(track: u32) -> i32 {
2355 ffi::enter(move || {
2356 let track = ffi::parse_id(track)?;
2357 State::lock().consume.track_close(track)
2358 })
2359}
2360
2361/// Consume an audio track from a broadcast, emitting the frames in order.
2362///
2363/// `on_frame` is called with a positive frame ID per frame, then exactly once
2364/// more with a terminal code: `0` (closed cleanly) or a negative error. After
2365/// the terminal (`<= 0`) callback, `on_frame` is never called again and
2366/// `user_data` is never touched again, so release `user_data` there. The
2367/// terminal callback fires even after [moq_consume_audio_close].
2368/// The `max_latency_ms` parameter controls how long to wait before skipping frames.
2369///
2370/// Returns a non-zero handle to the track on success, or a negative code on failure.
2371///
2372/// # Safety
2373/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
2374#[unsafe(no_mangle)]
2375pub unsafe extern "C" fn moq_consume_audio(
2376 catalog: u32,
2377 index: u32,
2378 max_latency_ms: u64,
2379 on_frame: Option<extern "C" fn(user_data: *mut c_void, frame: i32)>,
2380 user_data: *mut c_void,
2381) -> i32 {
2382 ffi::enter(move || {
2383 let catalog = ffi::parse_id(catalog)?;
2384 let index = index as usize;
2385 let max_latency = std::time::Duration::from_millis(max_latency_ms);
2386 let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame) };
2387 State::lock().consume.audio(catalog, index, max_latency, on_frame)
2388 })
2389}
2390
2391/// Stop an audio track consumer's background task.
2392///
2393/// Returns immediately: zero on success, or a negative code if already closed.
2394/// Does NOT free `user_data`; the [moq_consume_audio] `on_frame` callback
2395/// still fires once more with a terminal `0` (or a negative error), which is
2396/// where `user_data` should be released.
2397#[unsafe(no_mangle)]
2398pub extern "C" fn moq_consume_audio_close(track: u32) -> i32 {
2399 ffi::enter(move || {
2400 let track = ffi::parse_id(track)?;
2401 State::lock().consume.track_close(track)
2402 })
2403}
2404
2405/// Get a chunk of a frame's payload.
2406///
2407/// Read the payload of a frame as a single contiguous slice.
2408///
2409/// Frames are not chunked; the entire payload is delivered through `dst.payload` /
2410/// `dst.payload_size` in one call. The pointer is valid until [`moq_consume_frame_free`]
2411/// is called for this frame.
2412///
2413/// Returns a zero on success, or a negative code on failure.
2414///
2415/// # Safety
2416/// - The caller must ensure that `dst` is a valid pointer to a [moq_frame] struct.
2417#[unsafe(no_mangle)]
2418pub unsafe extern "C" fn moq_consume_frame(frame: u32, dst: *mut moq_frame) -> i32 {
2419 ffi::enter(move || {
2420 let frame = ffi::parse_id(frame)?;
2421 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2422 State::lock().consume.frame(frame, dst)
2423 })
2424}
2425
2426/// Free a decoded frame delivered via a [moq_consume_video] or [moq_consume_audio] callback.
2427///
2428/// Returns a zero on success, or a negative code on failure.
2429#[unsafe(no_mangle)]
2430pub extern "C" fn moq_consume_frame_free(frame: u32) -> i32 {
2431 ffi::enter(move || {
2432 let frame = ffi::parse_id(frame)?;
2433 State::lock().consume.frame_close(frame)
2434 })
2435}
2436
2437/// Close a broadcast consumer and clean up its resources.
2438///
2439/// Returns a zero on success, or a negative code on failure.
2440#[unsafe(no_mangle)]
2441pub extern "C" fn moq_consume_close(consume: u32) -> i32 {
2442 ffi::enter(move || {
2443 let consume = ffi::parse_id(consume)?;
2444 State::lock().consume.close(consume)
2445 })
2446}
2447
2448/// Subscribe to a raw track by name, delivering each frame's payload as-is.
2449///
2450/// This is the counterpart to [moq_publish_track]: no catalog lookup or
2451/// container parsing. `on_frame` is called with a positive raw frame ID for each
2452/// frame in sequence order, then exactly once more with a terminal code: `0`
2453/// (closed cleanly) or a negative error. After the terminal (`<= 0`) callback,
2454/// `on_frame` is never called again and `user_data` is never touched again, so
2455/// release `user_data` there. The terminal callback fires even after
2456/// [moq_consume_track_close]. Read each frame with [moq_consume_track_frame] and
2457/// release it with [moq_consume_track_frame_free]. Pass NULL for `subscription`
2458/// to use moq-net defaults.
2459///
2460/// Returns a non-zero handle to the track on success, or a negative code on failure.
2461///
2462/// # Safety
2463/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
2464/// - The caller must ensure that subscription is either NULL or a valid pointer to a [moq_subscription] struct.
2465/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
2466#[unsafe(no_mangle)]
2467pub unsafe extern "C" fn moq_consume_track(
2468 broadcast: u32,
2469 name: *const c_char,
2470 name_len: usize,
2471 subscription: *const moq_subscription,
2472 on_frame: Option<extern "C" fn(user_data: *mut c_void, frame: i32)>,
2473 user_data: *mut c_void,
2474) -> i32 {
2475 ffi::enter(move || {
2476 let broadcast = ffi::parse_id(broadcast)?;
2477 let name = unsafe { ffi::parse_str(name, name_len)? };
2478 let subscription = unsafe { subscription.as_ref() }.map(moq_net::track::Subscription::from);
2479 let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame) };
2480 State::lock().consume.raw_track(broadcast, name, subscription, on_frame)
2481 })
2482}
2483
2484/// Update a raw track subscription's delivery preferences.
2485///
2486/// Pass NULL for `subscription` to reset to moq-net defaults.
2487///
2488/// Returns a zero on success, or a negative code on failure.
2489///
2490/// # Safety
2491/// - The caller must ensure that subscription is either NULL or a valid pointer to a [moq_subscription] struct.
2492#[unsafe(no_mangle)]
2493pub unsafe extern "C" fn moq_consume_track_update(track: u32, subscription: *const moq_subscription) -> i32 {
2494 ffi::enter(move || {
2495 let track = ffi::parse_id(track)?;
2496 let subscription = unsafe { subscription.as_ref() }.map(moq_net::track::Subscription::from);
2497 State::lock().consume.raw_track_update(track, subscription)
2498 })
2499}
2500
2501/// Read a raw frame's payload delivered via the [moq_consume_track] callback.
2502///
2503/// Fills `dst.payload` / `dst.payload_size`; the pointer is valid until the
2504/// frame is released with [moq_consume_frame_free]. `dst.timestamp_us` is the
2505/// frame presentation timestamp in microseconds. `dst.keyframe` is reported as
2506/// false because raw tracks do not parse codec metadata.
2507///
2508/// Returns a zero on success, or a negative code on failure.
2509///
2510/// # Safety
2511/// - The caller must ensure that `dst` is a valid pointer to a [moq_frame] struct.
2512#[unsafe(no_mangle)]
2513pub unsafe extern "C" fn moq_consume_track_frame(frame: u32, dst: *mut moq_frame) -> i32 {
2514 ffi::enter(move || {
2515 let frame = ffi::parse_id(frame)?;
2516 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2517 State::lock().consume.raw_frame(frame, dst)
2518 })
2519}
2520
2521/// Free a raw frame delivered via the [moq_consume_track] callback, releasing its payload.
2522///
2523/// Returns a zero on success, or a negative code on failure.
2524#[unsafe(no_mangle)]
2525pub extern "C" fn moq_consume_track_frame_free(frame: u32) -> i32 {
2526 ffi::enter(move || {
2527 let frame = ffi::parse_id(frame)?;
2528 State::lock().consume.raw_frame_close(frame)
2529 })
2530}
2531
2532/// Stop a raw track consumer's background task.
2533///
2534/// Returns immediately: zero on success, or a negative code if already closed.
2535/// Does NOT free `user_data`; the [moq_consume_track] `on_frame` callback still
2536/// fires once more with a terminal `0` (or a negative error), which is where
2537/// `user_data` should be released. Frames already delivered via the callback
2538/// remain valid until released with [moq_consume_track_frame_free].
2539#[unsafe(no_mangle)]
2540pub extern "C" fn moq_consume_track_close(track: u32) -> i32 {
2541 ffi::enter(move || {
2542 let track = ffi::parse_id(track)?;
2543 State::lock().consume.raw_track_close(track)
2544 })
2545}
2546
2547/// Subscribe to a raw track's best-effort datagrams by name.
2548///
2549/// The datagram counterpart to [moq_consume_track], on its own subscription. `on_datagram`
2550/// is called with a positive datagram ID for each datagram in arrival order, then exactly
2551/// once more with a terminal code: `0` (closed cleanly) or a negative error. After the
2552/// terminal (`<= 0`) callback, `on_datagram` is never called again and `user_data` is never
2553/// touched again, so release `user_data` there. The terminal callback fires even after
2554/// [moq_consume_datagrams_close]. Read each datagram with [moq_consume_datagram] and release
2555/// it with [moq_consume_datagram_free]. Datagrams arrive only over datagram-capable
2556/// transports and lite-05 or newer moq-lite; there is no stream fallback.
2557///
2558/// Returns a non-zero handle to the subscription on success, or a negative code on failure.
2559///
2560/// # Safety
2561/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
2562/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_datagram` callback.
2563#[unsafe(no_mangle)]
2564pub unsafe extern "C" fn moq_consume_datagrams(
2565 broadcast: u32,
2566 name: *const c_char,
2567 name_len: usize,
2568 on_datagram: Option<extern "C" fn(user_data: *mut c_void, datagram: i32)>,
2569 user_data: *mut c_void,
2570) -> i32 {
2571 ffi::enter(move || {
2572 let broadcast = ffi::parse_id(broadcast)?;
2573 let name = unsafe { ffi::parse_str(name, name_len)? };
2574 let on_datagram = unsafe { ffi::OnStatus::new(user_data, on_datagram) };
2575 State::lock().consume.datagram_track(broadcast, name, on_datagram)
2576 })
2577}
2578
2579/// Read a datagram delivered via the [moq_consume_datagrams] callback.
2580///
2581/// Fills `dst.payload` / `dst.payload_size` (valid until the datagram is released with
2582/// [moq_consume_datagram_free]), plus `dst.timestamp_us` and `dst.sequence`.
2583///
2584/// Returns a zero on success, or a negative code on failure.
2585///
2586/// # Safety
2587/// - The caller must ensure that `dst` is a valid pointer to a [moq_datagram] struct.
2588#[unsafe(no_mangle)]
2589pub unsafe extern "C" fn moq_consume_datagram(datagram: u32, dst: *mut moq_datagram) -> i32 {
2590 ffi::enter(move || {
2591 let datagram = ffi::parse_id(datagram)?;
2592 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2593 State::lock().consume.datagram(datagram, dst)
2594 })
2595}
2596
2597/// Free a datagram delivered via the [moq_consume_datagrams] callback, releasing its payload.
2598///
2599/// Returns a zero on success, or a negative code on failure.
2600#[unsafe(no_mangle)]
2601pub extern "C" fn moq_consume_datagram_free(datagram: u32) -> i32 {
2602 ffi::enter(move || {
2603 let datagram = ffi::parse_id(datagram)?;
2604 State::lock().consume.datagram_close(datagram)
2605 })
2606}
2607
2608/// Stop a datagram subscription's background task.
2609///
2610/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
2611/// `user_data`; the [moq_consume_datagrams] `on_datagram` callback still fires once more with a
2612/// terminal `0` (or a negative error), which is where `user_data` should be released. Datagrams
2613/// already delivered via the callback remain valid until released with [moq_consume_datagram_free].
2614#[unsafe(no_mangle)]
2615pub extern "C" fn moq_consume_datagrams_close(task: u32) -> i32 {
2616 ffi::enter(move || {
2617 let task = ffi::parse_id(task)?;
2618 State::lock().consume.datagram_track_close(task)
2619 })
2620}
2621
2622/// Subscribe to a JSON snapshot track (lossy latest-value) by name.
2623///
2624/// `on_value` is called with a positive value ID for each new latest value; a consumer that
2625/// falls behind collapses the backlog and only sees the newest. It is called exactly once more
2626/// with a terminal `0` (track ended / closed) or a negative error, after which `user_data` is
2627/// never touched again, so release it there. Read each value with [moq_consume_json_value] and
2628/// release it with [moq_consume_json_value_free]. Pass the same compression the producer used.
2629///
2630/// Returns a non-zero handle to the task on success, or a negative code on failure.
2631///
2632/// # Safety
2633/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
2634/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_value` callback.
2635#[unsafe(no_mangle)]
2636pub unsafe extern "C" fn moq_consume_json_snapshot(
2637 broadcast: u32,
2638 name: *const c_char,
2639 name_len: usize,
2640 config: *const moq_json_snapshot_config,
2641 on_value: Option<extern "C" fn(user_data: *mut c_void, value: i32)>,
2642 user_data: *mut c_void,
2643) -> i32 {
2644 ffi::enter(move || {
2645 let broadcast = ffi::parse_id(broadcast)?;
2646 let name = unsafe { ffi::parse_str(name, name_len)? };
2647 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
2648 let mut consumer = moq_json::snapshot::ConsumerConfig::default();
2649 consumer.compression = config.compression;
2650 let on_value = unsafe { ffi::OnStatus::new(user_data, on_value) };
2651 State::lock().consume.json_snapshot(broadcast, name, consumer, on_value)
2652 })
2653}
2654
2655/// Subscribe to a JSON stream track (lossless append-log) by name.
2656///
2657/// `on_value` is called with a positive value ID for each record, in order, then once more with
2658/// a terminal `0` or negative error where `user_data` should be released. Read each value with
2659/// [moq_consume_json_value] and release it with [moq_consume_json_value_free].
2660///
2661/// Returns a non-zero handle to the task on success, or a negative code on failure.
2662///
2663/// # Safety
2664/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
2665/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_value` callback.
2666#[unsafe(no_mangle)]
2667pub unsafe extern "C" fn moq_consume_json_stream(
2668 broadcast: u32,
2669 name: *const c_char,
2670 name_len: usize,
2671 config: *const moq_json_stream_config,
2672 on_value: Option<extern "C" fn(user_data: *mut c_void, value: i32)>,
2673 user_data: *mut c_void,
2674) -> i32 {
2675 ffi::enter(move || {
2676 let broadcast = ffi::parse_id(broadcast)?;
2677 let name = unsafe { ffi::parse_str(name, name_len)? };
2678 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
2679 let consumer = moq_json::stream::ConsumerConfig::default().with_compression(config.compression);
2680 let on_value = unsafe { ffi::OnStatus::new(user_data, on_value) };
2681 State::lock().consume.json_stream(broadcast, name, consumer, on_value)
2682 })
2683}
2684
2685/// Read a JSON value delivered via a [moq_consume_json_snapshot] or [moq_consume_json_stream] callback.
2686///
2687/// Fills `dst.json` / `dst.json_len`; the pointer is valid until the value is released with
2688/// [moq_consume_json_value_free].
2689///
2690/// Returns a zero on success, or a negative code on failure.
2691///
2692/// # Safety
2693/// - The caller must ensure `dst` is a valid pointer to a [moq_json_value] struct.
2694#[unsafe(no_mangle)]
2695pub unsafe extern "C" fn moq_consume_json_value(value: u32, dst: *mut moq_json_value) -> i32 {
2696 ffi::enter(move || {
2697 let value = ffi::parse_id(value)?;
2698 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2699 State::lock().consume.json_value(value, dst)
2700 })
2701}
2702
2703/// Release a JSON value delivered via a consumer callback.
2704///
2705/// Returns a zero on success, or a negative code on failure.
2706#[unsafe(no_mangle)]
2707pub extern "C" fn moq_consume_json_value_free(value: u32) -> i32 {
2708 ffi::enter(move || {
2709 let value = ffi::parse_id(value)?;
2710 State::lock().consume.json_value_close(value)
2711 })
2712}
2713
2714/// Stop a JSON consumer's background task (snapshot or stream).
2715///
2716/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
2717/// `user_data`; the `on_value` callback still fires once more with a terminal `0` (or a negative
2718/// error), which is where `user_data` should be released. Values already delivered remain valid
2719/// until released with [moq_consume_json_value_free].
2720#[unsafe(no_mangle)]
2721pub extern "C" fn moq_consume_json_close(task: u32) -> i32 {
2722 ffi::enter(move || {
2723 let task = ffi::parse_id(task)?;
2724 State::lock().consume.json_close(task)
2725 })
2726}