moq/api.rs
1use crate::ffi::ReturnCode;
2use crate::{Connect, Error, State, ffi, moq_protocol_error};
3
4use std::ffi::c_char;
5use std::ffi::c_void;
6use std::str::FromStr;
7
8use tracing::Level;
9
10/// How a media track's frames are wrapped, independent of the codec.
11///
12/// The ABI carries this as a `uint32_t`, so an unknown discriminant from C is an
13/// error rather than UB.
14#[repr(C)]
15#[allow(non_camel_case_types)]
16#[derive(Clone, Copy, Debug)]
17pub enum moq_container_kind {
18 /// A QUIC VarInt timestamp prefix followed by the raw codec payload.
19 /// Timestamps are in microseconds.
20 MOQ_CONTAINER_KIND_LEGACY = 0,
21 /// Fragmented MP4: each frame is a complete moof+mdat fragment, described by
22 /// the init segment in `moq_container::init`.
23 MOQ_CONTAINER_KIND_CMAF = 1,
24 /// Low Overhead Container (draft-ietf-moq-loc): a small property block
25 /// followed by the codec payload.
26 MOQ_CONTAINER_KIND_LOC = 2,
27 /// A container this build does not recognize, so the rendition must be
28 /// ignored. Only ever read out of a catalog: publishing it is an error.
29 MOQ_CONTAINER_KIND_UNKNOWN = 3,
30}
31
32/// The container of a video or audio rendition, plus whatever that container
33/// needs to describe itself.
34///
35/// Zeroing this struct means `MOQ_CONTAINER_KIND_LEGACY` with no init segment,
36/// which is what a rendition written by [moq_publish_audio] or [moq_publish_video] carries.
37#[repr(C)]
38#[allow(non_camel_case_types)]
39#[derive(Clone, Copy)]
40pub struct moq_container {
41 /// `moq_container_kind` discriminant.
42 pub kind: u32,
43
44 /// The CMAF init segment (ftyp+moov), or NULL.
45 /// Read only when `kind` is `MOQ_CONTAINER_KIND_CMAF`, where it is required.
46 pub init: *const u8,
47 pub init_len: usize,
48}
49
50impl Default for moq_container {
51 fn default() -> Self {
52 Self {
53 kind: moq_container_kind::MOQ_CONTAINER_KIND_LEGACY as u32,
54 init: std::ptr::null(),
55 init_len: 0,
56 }
57 }
58}
59
60/// # Safety
61/// - `container->init` must point to `container->init_len` bytes when
62/// `container->kind` is `MOQ_CONTAINER_KIND_CMAF`.
63pub(crate) unsafe fn parse_container(container: &moq_container) -> Result<hang::catalog::Container, Error> {
64 use hang::catalog::Container;
65
66 Ok(match container.kind {
67 v if v == moq_container_kind::MOQ_CONTAINER_KIND_LEGACY as u32 => Container::Legacy,
68 v if v == moq_container_kind::MOQ_CONTAINER_KIND_CMAF as u32 => {
69 let init = unsafe { ffi::parse_slice(container.init, container.init_len)? };
70 // A CMAF rendition is undecodable without its init segment, so an empty one
71 // fails here rather than at every subscriber.
72 if init.is_empty() {
73 return Err(Error::InvalidPointer);
74 }
75
76 Container::Cmaf {
77 init: bytes::Bytes::copy_from_slice(init),
78 }
79 }
80 v if v == moq_container_kind::MOQ_CONTAINER_KIND_LOC as u32 => Container::Loc,
81 // UNKNOWN included: we kept none of the original JSON, so there is nothing to republish.
82 _ => return Err(Error::InvalidCode),
83 })
84}
85
86/// Describe a catalog container for C, borrowing the CMAF init segment rather
87/// than copying it, so the result lives only as long as the catalog snapshot.
88pub(crate) fn borrow_container(container: &hang::catalog::Container) -> moq_container {
89 use hang::catalog::Container;
90
91 let (kind, init) = match container {
92 Container::Legacy => (moq_container_kind::MOQ_CONTAINER_KIND_LEGACY, None),
93 Container::Cmaf { init } => (moq_container_kind::MOQ_CONTAINER_KIND_CMAF, Some(init)),
94 Container::Loc => (moq_container_kind::MOQ_CONTAINER_KIND_LOC, None),
95 Container::Unknown(_) => (moq_container_kind::MOQ_CONTAINER_KIND_UNKNOWN, None),
96 };
97
98 moq_container {
99 kind: kind as u32,
100 init: init.map_or(std::ptr::null(), |init| init.as_ptr()),
101 init_len: init.map_or(0, |init| init.len()),
102 }
103}
104
105/// A single audio codec [moq_publish_audio] can parse.
106#[repr(C)]
107#[allow(non_camel_case_types)]
108#[derive(Clone, Copy, Debug, PartialEq, Eq)]
109pub enum moq_audio_format {
110 /// Advanced Audio Coding, configured by an AudioSpecificConfig.
111 MOQ_AUDIO_FORMAT_AAC = 0,
112 /// Opus, configured by an OpusHead.
113 MOQ_AUDIO_FORMAT_OPUS = 1,
114 /// FLAC, configured by the `fLaC` marker plus its STREAMINFO block.
115 MOQ_AUDIO_FORMAT_FLAC = 2,
116 /// MPEG-1/2 Audio Layer III.
117 MOQ_AUDIO_FORMAT_MP3 = 3,
118}
119
120/// A single video codec [moq_publish_video] can parse.
121///
122/// H.264 and H.265 appear twice each because the framing differs, not just the
123/// codec: AVC1/HVC1 are length-prefixed with an out-of-band config record,
124/// while AVC3/HEV1 are Annex-B with the parameter sets inline.
125#[repr(C)]
126#[allow(non_camel_case_types)]
127#[derive(Clone, Copy, Debug, PartialEq, Eq)]
128pub enum moq_video_format {
129 /// H.264, length-prefixed NALUs with an out-of-band avcC.
130 MOQ_VIDEO_FORMAT_AVC1 = 0,
131 /// H.264, Annex-B with inline SPS/PPS.
132 MOQ_VIDEO_FORMAT_AVC3 = 1,
133 /// H.265, length-prefixed NALUs with an out-of-band hvcC.
134 MOQ_VIDEO_FORMAT_HVC1 = 2,
135 /// H.265, Annex-B with inline parameter sets.
136 MOQ_VIDEO_FORMAT_HEV1 = 3,
137 /// AV1.
138 MOQ_VIDEO_FORMAT_AV01 = 4,
139 /// VP8.
140 MOQ_VIDEO_FORMAT_VP8 = 5,
141 /// VP9.
142 MOQ_VIDEO_FORMAT_VP9 = 6,
143}
144
145/// A container [moq_publish_container] can demux, which may publish several tracks.
146#[repr(C)]
147#[allow(non_camel_case_types)]
148#[derive(Clone, Copy, Debug, PartialEq, Eq)]
149pub enum moq_container_format {
150 /// Fragmented MP4 / CMAF.
151 MOQ_CONTAINER_FORMAT_FMP4 = 0,
152 /// Matroska / WebM.
153 MOQ_CONTAINER_FORMAT_MKV = 1,
154 /// MPEG-2 transport stream.
155 MOQ_CONTAINER_FORMAT_TS = 2,
156 /// Flash Video, as used by RTMP.
157 MOQ_CONTAINER_FORMAT_FLV = 3,
158}
159
160/// Configuration for [moq_publish_audio].
161///
162/// Zero the struct, then set `format` and the required `init` bytes. New
163/// optional fields are appended so existing initializers keep their meaning.
164#[repr(C)]
165#[allow(non_camel_case_types)]
166pub struct moq_audio_init {
167 /// The audio codec, a [moq_audio_format] value.
168 pub format: u32,
169
170 /// Codec init bytes: an OpusHead, an AudioSpecificConfig, a STREAMINFO.
171 /// Required, since audio has no in-band config to resolve from frames.
172 pub init: *const u8,
173 /// Length of `init` in bytes.
174 pub init_len: usize,
175
176 /// Human-readable rendition name for track pickers, or NULL if not used.
177 pub label: *const c_char,
178 /// Length of `label` in bytes.
179 pub label_len: usize,
180}
181
182/// Configuration for [moq_publish_video].
183///
184/// Zero the struct, then set `format` and whatever else the codec needs. `init`
185/// may stay NULL for a format that resolves in band.
186#[repr(C)]
187#[allow(non_camel_case_types)]
188pub struct moq_video_init {
189 /// The video codec, a [moq_video_format] value.
190 pub format: u32,
191
192 /// Codec init bytes (an avcC, an hvcC), or NULL for a format that resolves
193 /// from the stream itself.
194 pub init: *const u8,
195 /// Length of `init` in bytes.
196 pub init_len: usize,
197
198 /// Human-readable rendition name for track pickers, or NULL if not used.
199 pub label: *const c_char,
200 /// Length of `label` in bytes.
201 pub label_len: usize,
202
203 /// Catalog fields the bitstream cannot reveal itself. Zeroed means none.
204 pub hint: moq_video_hint,
205}
206
207/// Optional catalog fields for [moq_video_init::hint].
208///
209/// Zero the struct and set only the `has_*` flags you want. Hints fill gaps the
210/// bitstream leaves (especially bitrate); a value the stream detects later wins
211/// for dimensions.
212#[repr(C)]
213#[allow(non_camel_case_types)]
214#[derive(Clone, Copy, Default)]
215pub struct moq_video_hint {
216 /// Encoded width in pixels when `has_coded` is true.
217 pub coded_width: u32,
218 /// Encoded height in pixels when `has_coded` is true.
219 pub coded_height: u32,
220 /// Whether `coded_width` and `coded_height` are present.
221 pub has_coded: bool,
222
223 /// Maximum bitrate in bits per second when `has_bitrate` is true.
224 pub bitrate: u64,
225 /// Whether `bitrate` is present.
226 pub has_bitrate: bool,
227
228 /// Frame rate when `has_framerate` is true.
229 pub framerate: f64,
230 /// Whether `framerate` is present.
231 pub has_framerate: bool,
232
233 /// Latency-optimized decode when `has_optimize_for_latency` is true.
234 pub optimize_for_latency: bool,
235 /// Whether `optimize_for_latency` is present.
236 pub has_optimize_for_latency: bool,
237}
238
239impl moq_video_hint {
240 /// The catalog hint these flags describe.
241 fn resolve(&self) -> moq_mux::catalog::VideoHint {
242 let mut out = moq_mux::catalog::VideoHint::default();
243 if self.has_coded {
244 out.coded_width = Some(self.coded_width);
245 out.coded_height = Some(self.coded_height);
246 }
247 if self.has_bitrate {
248 out.bitrate = Some(self.bitrate);
249 }
250 if self.has_framerate {
251 out.framerate = Some(self.framerate);
252 }
253 if self.has_optimize_for_latency {
254 out.optimize_for_latency = Some(self.optimize_for_latency);
255 }
256 out
257 }
258}
259
260/// Configuration for [moq_publish_container].
261///
262/// There is no label here: a container publishes and describes its own tracks,
263/// so a rendition name would have no single track to land on.
264#[repr(C)]
265#[allow(non_camel_case_types)]
266pub struct moq_container_init {
267 /// The container format, a [moq_container_format] value.
268 pub format: u32,
269
270 /// The leading chunk of the container, decoded immediately, or NULL.
271 pub init: *const u8,
272 /// Length of `init` in bytes.
273 pub init_len: usize,
274}
275
276/// Validate an audio format code from C.
277///
278/// The field is a `u32` rather than the enum: C can put any integer there, and matching an
279/// out-of-range discriminant as a Rust enum is UB. Same reason as [moq_audio_sample_format].
280fn audio_format_from_u32(value: u32) -> Result<moq_mux::import::AudioFormat, Error> {
281 use moq_mux::import::AudioFormat;
282 Ok(match value {
283 v if v == moq_audio_format::MOQ_AUDIO_FORMAT_AAC as u32 => AudioFormat::Aac,
284 v if v == moq_audio_format::MOQ_AUDIO_FORMAT_OPUS as u32 => AudioFormat::Opus,
285 v if v == moq_audio_format::MOQ_AUDIO_FORMAT_FLAC as u32 => AudioFormat::Flac,
286 v if v == moq_audio_format::MOQ_AUDIO_FORMAT_MP3 as u32 => AudioFormat::Mp3,
287 _ => return Err(Error::InvalidCode),
288 })
289}
290
291/// Validate a video format code from C. See [audio_format_from_u32].
292fn video_format_from_u32(value: u32) -> Result<moq_mux::import::VideoFormat, Error> {
293 use moq_mux::import::VideoFormat;
294 Ok(match value {
295 v if v == moq_video_format::MOQ_VIDEO_FORMAT_AVC1 as u32 => VideoFormat::Avc1,
296 v if v == moq_video_format::MOQ_VIDEO_FORMAT_AVC3 as u32 => VideoFormat::Avc3,
297 v if v == moq_video_format::MOQ_VIDEO_FORMAT_HVC1 as u32 => VideoFormat::Hvc1,
298 v if v == moq_video_format::MOQ_VIDEO_FORMAT_HEV1 as u32 => VideoFormat::Hev1,
299 v if v == moq_video_format::MOQ_VIDEO_FORMAT_AV01 as u32 => VideoFormat::Av01,
300 v if v == moq_video_format::MOQ_VIDEO_FORMAT_VP8 as u32 => VideoFormat::Vp8,
301 v if v == moq_video_format::MOQ_VIDEO_FORMAT_VP9 as u32 => VideoFormat::Vp9,
302 _ => return Err(Error::InvalidCode),
303 })
304}
305
306/// Validate a container format code from C. See [audio_format_from_u32].
307fn container_format_from_u32(value: u32) -> Result<moq_mux::import::ContainerFormat, Error> {
308 use moq_mux::import::ContainerFormat;
309 Ok(match value {
310 v if v == moq_container_format::MOQ_CONTAINER_FORMAT_FMP4 as u32 => ContainerFormat::Fmp4,
311 v if v == moq_container_format::MOQ_CONTAINER_FORMAT_MKV as u32 => ContainerFormat::Mkv,
312 v if v == moq_container_format::MOQ_CONTAINER_FORMAT_TS as u32 => ContainerFormat::Ts,
313 v if v == moq_container_format::MOQ_CONTAINER_FORMAT_FLV as u32 => ContainerFormat::Flv,
314 _ => return Err(Error::InvalidCode),
315 })
316}
317
318/// Information about a video rendition in the catalog.
319#[repr(C)]
320#[allow(non_camel_case_types)]
321pub struct moq_video_config {
322 /// The name of the track, NOT NULL terminated.
323 pub name: *const c_char,
324 pub name_len: usize,
325
326 /// The codec of the track, NOT NULL terminated
327 pub codec: *const c_char,
328 pub codec_len: usize,
329
330 /// The description of the track, or NULL if not used.
331 /// This is codec specific, for example H264:
332 /// - NULL: annex.b encoded
333 /// - Non-NULL: AVCC encoded
334 pub description: *const u8,
335 pub description_len: usize,
336
337 /// The encoded width/height of the media, a hint so a decoder can size its
338 /// buffers up front. Zero means absent, which no valid dimension is, so the
339 /// two are independent: a catalog carrying only one round-trips unchanged.
340 pub coded_width: u32,
341 pub coded_height: u32,
342
343 /// How the track's frames are wrapped.
344 pub container: moq_container,
345
346 /// Human-readable rendition name for track pickers, or NULL if not used.
347 pub label: *const c_char,
348 /// Length of `label` in bytes.
349 pub label_len: usize,
350}
351
352/// Catalog properties shared by every video rendition.
353///
354/// A false `has_*` flag clears that field from the next catalog rather than preserving its previous value.
355#[repr(C)]
356#[allow(non_camel_case_types)]
357#[derive(Clone, Copy, Default)]
358pub struct moq_video_properties {
359 /// Final rendered width in pixels when `has_display` is true.
360 pub display_width: u32,
361
362 /// Final rendered height in pixels when `has_display` is true.
363 pub display_height: u32,
364
365 /// Whether `display_width` and `display_height` are present.
366 pub has_display: bool,
367
368 /// Clockwise rotation in degrees when `has_rotation` is true.
369 pub rotation: f64,
370
371 /// Whether `rotation` is present.
372 pub has_rotation: bool,
373
374 /// Whether to flip horizontally after rotation when `has_flip` is true.
375 pub flip: bool,
376
377 /// Whether `flip` is present.
378 pub has_flip: bool,
379}
380
381/// Information about an audio rendition in the catalog.
382#[repr(C)]
383#[allow(non_camel_case_types)]
384pub struct moq_audio_config {
385 /// The name of the track, NOT NULL terminated
386 pub name: *const c_char,
387 pub name_len: usize,
388
389 /// The codec of the track, NOT NULL terminated
390 pub codec: *const c_char,
391 pub codec_len: usize,
392
393 /// The description of the track, or NULL if not used.
394 pub description: *const u8,
395 pub description_len: usize,
396
397 /// The sample rate of the track in Hz
398 pub sample_rate: u32,
399
400 /// The number of channels in the track
401 pub channel_count: u32,
402
403 /// How the track's frames are wrapped.
404 pub container: moq_container,
405
406 /// Human-readable rendition name for track pickers, or NULL if not used.
407 pub label: *const c_char,
408 /// Length of `label` in bytes.
409 pub label_len: usize,
410}
411
412/// Options for a JSON snapshot track (lossy latest-value mode).
413///
414/// The same config is passed to a producer and its consumers, but the consumer reads only
415/// `compression`; `delta_ratio` is producer-only.
416#[repr(C)]
417#[allow(non_camel_case_types)]
418pub struct moq_json_snapshot_config {
419 /// How aggressively the producer emits deltas instead of full snapshots. `0` disables deltas
420 /// (one snapshot per group); a positive value allows roughly that many snapshots' worth of
421 /// deltas before rolling. Ignored by the consumer.
422 pub delta_ratio: u32,
423
424 /// DEFLATE-compress each group. Must match on the producer and consumer.
425 pub compression: bool,
426}
427
428/// Options for a JSON stream track (lossless append-log mode).
429#[repr(C)]
430#[allow(non_camel_case_types)]
431pub struct moq_json_stream_config {
432 /// DEFLATE-compress the group. Must match on the producer and consumer.
433 pub compression: bool,
434}
435
436/// A JSON value delivered by a consumer callback.
437#[repr(C)]
438#[allow(non_camel_case_types)]
439pub struct moq_json_value {
440 /// The JSON document as UTF-8, NOT NULL terminated.
441 pub json: *const c_char,
442 pub json_len: usize,
443}
444
445/// Information about a frame of media.
446#[repr(C)]
447#[allow(non_camel_case_types)]
448pub struct moq_frame {
449 /// The payload of the frame, or NULL/0 if the stream has ended
450 pub payload: *const u8,
451 pub payload_size: usize,
452
453 /// The presentation timestamp of the frame in microseconds
454 pub timestamp_us: u64,
455
456 /// Whether this frame opens a group or is a video keyframe; audio is true only at a group start.
457 pub keyframe: bool,
458}
459
460/// A best-effort raw track datagram delivered via [moq_consume_datagrams].
461#[repr(C)]
462#[allow(non_camel_case_types)]
463pub struct moq_datagram {
464 /// The payload of the datagram, or NULL/0 if the track has ended.
465 pub payload: *const u8,
466 pub payload_size: usize,
467
468 /// The presentation timestamp of the datagram in microseconds.
469 pub timestamp_us: u64,
470
471 /// Per-track sequence number, drawn from the same namespace as groups.
472 pub sequence: u64,
473}
474
475/// Publisher-side raw track properties.
476///
477/// A null [moq_publish_track] `info` pointer uses the moq-net defaults.
478/// A zero-initialized struct also uses those defaults, except `priority` where
479/// zero is the default itself.
480#[repr(C)]
481#[allow(non_camel_case_types)]
482pub struct moq_track_info {
483 /// Priority, used to break ties between subscriptions of equal subscriber priority.
484 pub priority: u8,
485
486 /// Maximum age of a non-latest group before the publisher evicts it, in microseconds.
487 /// The publisher-side half of `moq_subscription.max_age_us`.
488 pub max_age_us: u64,
489 /// Whether `max_age_us` is set. When false, the publisher's default applies.
490 pub max_age_present: bool,
491
492 /// Per-frame timescale in ticks per second.
493 pub timescale: u64,
494 /// Whether `timescale` is set. When false, the default microsecond timescale
495 /// applies, matching the `timestamp_us` units used everywhere else in this ABI.
496 pub timescale_present: bool,
497}
498
499impl TryFrom<&moq_track_info> for moq_net::track::Info {
500 type Error = Error;
501
502 fn try_from(info: &moq_track_info) -> Result<Self, Self::Error> {
503 // Raw tracks default to a microsecond timescale, matching the C ABI's
504 // timestamp_us units. An explicit timescale below overrides it.
505 let mut out = moq_net::track::Info::default()
506 .with_timescale(moq_net::Timescale::MICRO)
507 .with_priority(info.priority);
508 if info.max_age_present {
509 out = out.with_max_age(std::time::Duration::from_micros(info.max_age_us));
510 }
511 if info.timescale_present {
512 out = out.with_timescale(moq_net::Timescale::new(info.timescale)?);
513 }
514 Ok(out)
515 }
516}
517
518/// Whether a published track has subscribers, as reported by a demand watcher.
519///
520/// The positive values an `on_demand` callback receives; `0` and negative codes are
521/// the terminal statuses every callback shares.
522#[repr(C)]
523#[allow(non_camel_case_types)]
524#[derive(Clone, Copy, Debug)]
525pub enum moq_demand {
526 /// At least one subscriber is active.
527 MOQ_DEMAND_USED = 1,
528 /// No subscriber is active.
529 MOQ_DEMAND_UNUSED = 2,
530}
531
532/// Subscriber-side raw track delivery preferences.
533///
534/// A null [moq_consume_track] or [moq_consume_track_update] `subscription`
535/// pointer uses the moq-net defaults.
536#[repr(C)]
537#[allow(non_camel_case_types)]
538pub struct moq_subscription {
539 /// Delivery priority. Higher values preempt lower ones under contention.
540 pub priority: u8,
541
542 /// Maximum age of a non-latest group before it is skipped, in microseconds.
543 /// Zero skips immediately. Enforced by the publisher's cache and by any local buffering.
544 pub max_age_us: u64,
545
546 /// The lowest group to deliver (a floor). A floor is not a request: `max_age_us` is
547 /// what asks for data, and delivery starts at the oldest group at or above the floor
548 /// within that budget (the latest group at the default budget of 0).
549 pub group_start: u64,
550 /// Whether `group_start` is present. When false, there is no floor.
551 pub group_start_present: bool,
552
553 /// First group not to deliver (exclusive), or ignored when `group_end_present` is
554 /// false. `0` is the empty range.
555 pub group_end: u64,
556 /// Whether `group_end` is present. When false, there is no end cap.
557 pub group_end_present: bool,
558}
559
560impl From<&moq_subscription> for moq_net::track::Subscription {
561 fn from(subscription: &moq_subscription) -> Self {
562 let mut out = moq_net::track::Subscription::default()
563 .with_priority(subscription.priority)
564 .with_max_age(std::time::Duration::from_micros(subscription.max_age_us));
565 if subscription.group_start_present {
566 out = out.with_start(moq_net::track::Position::group(subscription.group_start));
567 }
568 if subscription.group_end_present {
569 out = out.with_end(moq_net::track::Position::group(subscription.group_end));
570 }
571 out
572 }
573}
574
575/// A borrowed UTF-8 string slice, NOT NULL terminated.
576///
577/// Used in both directions. As an output (e.g. a JSON document libmoq hands back) the
578/// pointer borrows libmoq's own storage and is only valid until the owning resource is
579/// freed; see the function that fills it for the exact lifetime. As an input (e.g. a
580/// [moq_client_config] list) the pointer borrows the caller's storage and is only read
581/// during the call.
582#[repr(C)]
583#[allow(non_camel_case_types)]
584#[derive(Clone, Copy)]
585pub struct moq_string {
586 /// Pointer to `len` bytes of UTF-8, NOT NULL terminated.
587 pub data: *const c_char,
588 pub len: usize,
589}
590
591/// One untyped application catalog section: a name and its JSON value.
592///
593/// Both `name` and `json` are UTF-8, NOT NULL terminated, and borrow the catalog
594/// snapshot's storage. They stay valid until the snapshot is freed with
595/// [moq_consume_catalog_free]. `json` is the section's value serialized as JSON
596/// (parse it yourself); a top-level catalog key beyond `video`/`audio`.
597#[repr(C)]
598#[allow(non_camel_case_types)]
599pub struct moq_section {
600 /// The section name, NOT NULL terminated.
601 pub name: *const c_char,
602 pub name_len: usize,
603
604 /// The section value as a JSON document, NOT NULL terminated.
605 pub json: *const c_char,
606 pub json_len: usize,
607}
608
609/// A route advertisement: hops and costs.
610///
611/// Pair with [moq_publish_announce] or [moq_origin_dynamic]. Zeroed (NULL hops,
612/// hops_len 0, cost 0) is the default route. `hops` is borrowed for the duration
613/// of the call that reads it.
614///
615/// `cost` is the warm price: what pulling via this route costs today, lower
616/// wins. `cold` is the same path undiscounted; when `has_cold` is false it
617/// defaults to `cost`, which is what a publisher seeding its production cost
618/// wants. New fields always append, so a zeroed struct keeps meaning the
619/// defaults.
620#[repr(C)]
621#[allow(non_camel_case_types)]
622#[derive(Clone, Copy)]
623pub struct moq_route {
624 /// Hop ids, oldest first. NULL when `hops_len` is 0. 0 is the anonymous
625 /// mark and is legal on a received chain.
626 pub hops: *const u64,
627 pub hops_len: usize,
628 /// Preference among routes covering the same prefix: lower wins.
629 pub cost: u64,
630 /// The same path with every warm discount removed. Ignored unless `has_cold`.
631 pub cold: u64,
632 /// Whether `cold` applies. When false, `cold` defaults to `cost`.
633 pub has_cold: bool,
634}
635
636impl Default for moq_route {
637 fn default() -> Self {
638 Self {
639 hops: std::ptr::null(),
640 hops_len: 0,
641 cost: 0,
642 cold: 0,
643 has_cold: false,
644 }
645 }
646}
647
648/// Parse a [moq_route], treating NULL as the default.
649///
650/// An omitted `cold` (`has_cold` false) prices the route undiscounted, like a
651/// publisher seeding its production cost.
652///
653/// # Safety
654/// `route` may be NULL, or must point at a readable [moq_route] whose `hops`
655/// pointer is valid for `hops_len` elements.
656unsafe fn parse_route(route: *const moq_route) -> Result<moq_net::origin::Route, Error> {
657 let Some(route) = (unsafe { route.as_ref() }) else {
658 return Ok(moq_net::origin::Route::default());
659 };
660 let cold = if route.has_cold { route.cold } else { route.cost };
661 let mut route_hops = moq_net::Hops::new();
662 if route.hops_len > 0 {
663 if route.hops.is_null() {
664 return Err(Error::InvalidPointer);
665 }
666 let hops = unsafe { std::slice::from_raw_parts(route.hops, route.hops_len) };
667 for id in hops {
668 let hop = if *id == 0 {
669 moq_net::Hop::UNKNOWN
670 } else {
671 moq_net::Hop::new(*id).map_err(|e| Error::InvalidConfig(e.to_string()))?
672 };
673 route_hops.push(hop).map_err(|e| Error::InvalidConfig(e.to_string()))?;
674 }
675 }
676 Ok(moq_net::origin::Route::default()
677 .with_cost(moq_net::origin::Cost { warm: route.cost, cold })
678 .with_hops(route_hops))
679}
680
681/// A route announcement or retraction from an origin.
682#[repr(C)]
683#[allow(non_camel_case_types)]
684pub struct moq_announce_update {
685 /// The covered prefix, relative to the origin, NOT NULL terminated
686 pub prefix: *const c_char,
687 pub prefix_len: usize,
688
689 /// What each requested filter wildcard matched. Each string is NOT NULL terminated.
690 /// Meaningful only when `has_captures` is true; false means the route overlaps
691 /// the filter without pinning every wildcard.
692 pub captures: *const moq_string,
693 pub captures_len: usize,
694 pub has_captures: bool,
695
696 /// Whether the route is active or was retracted
697 /// This MUST toggle between true and false over the lifetime of the route
698 pub active: bool,
699}
700
701/// Statistics and protocol sampled from the same connection by [moq_session_snapshot].
702#[repr(C)]
703#[allow(non_camel_case_types)]
704pub struct moq_connection_snapshot {
705 /// Transport statistics, with per-metric availability flags.
706 pub stats: moq_connection_stats,
707 /// Negotiated draft name, backed by static storage valid for the process lifetime.
708 pub protocol: moq_string,
709}
710
711/// A snapshot of connection statistics, filled in by [moq_session_stats].
712///
713/// Each metric has a `*_valid` flag: when `false`, the matching value is meaningless because
714/// the transport backend doesn't report it (a `false` flag is NOT the same as a zero value).
715/// Native QUIC reports every metric; the browser WebTransport reports few or none. Initialize
716/// the struct to zero before the call; [moq_session_stats] overwrites every field.
717#[repr(C)]
718#[allow(non_camel_case_types)]
719pub struct moq_connection_stats {
720 /// Smoothed round-trip time, in microseconds.
721 pub rtt_us: u64,
722 pub rtt_valid: bool,
723
724 /// Estimated send bandwidth from the congestion controller, in bits per second.
725 pub estimated_send_rate_bps: u64,
726 pub estimated_send_rate_valid: bool,
727
728 /// Estimated receive bandwidth from MoQ PROBE, in bits per second.
729 pub estimated_recv_rate_bps: u64,
730 pub estimated_recv_rate_valid: bool,
731
732 /// Total bytes sent, including retransmissions and overhead.
733 pub bytes_sent: u64,
734 pub bytes_sent_valid: bool,
735
736 /// Total bytes received, including duplicates and overhead.
737 pub bytes_received: u64,
738 pub bytes_received_valid: bool,
739
740 /// Total bytes lost (detected via retransmission or acknowledgement).
741 pub bytes_lost: u64,
742 pub bytes_lost_valid: bool,
743
744 /// Total datagrams sent.
745 pub packets_sent: u64,
746 pub packets_sent_valid: bool,
747
748 /// Total datagrams received.
749 pub packets_received: u64,
750 pub packets_received_valid: bool,
751
752 /// Total datagrams detected as lost.
753 pub packets_lost: u64,
754 pub packets_lost_valid: bool,
755}
756
757impl From<&moq_net::session::Stats> for moq_connection_stats {
758 fn from(stats: &moq_net::session::Stats) -> Self {
759 // An Option<u64> becomes a (value, valid) pair; absent metrics report 0/false.
760 fn split(value: Option<u64>) -> (u64, bool) {
761 (value.unwrap_or(0), value.is_some())
762 }
763
764 let (rtt_us, rtt_valid) = split(stats.rtt.map(|d| d.as_micros() as u64));
765 let (estimated_send_rate_bps, estimated_send_rate_valid) =
766 split(stats.estimated_send_rate.map(moq_net::bandwidth::Rate::as_bps));
767 let (estimated_recv_rate_bps, estimated_recv_rate_valid) =
768 split(stats.estimated_recv_rate.map(moq_net::bandwidth::Rate::as_bps));
769 let (bytes_sent, bytes_sent_valid) = split(stats.bytes_sent);
770 let (bytes_received, bytes_received_valid) = split(stats.bytes_received);
771 let (bytes_lost, bytes_lost_valid) = split(stats.bytes_lost);
772 let (packets_sent, packets_sent_valid) = split(stats.packets_sent);
773 let (packets_received, packets_received_valid) = split(stats.packets_received);
774 let (packets_lost, packets_lost_valid) = split(stats.packets_lost);
775
776 Self {
777 rtt_us,
778 rtt_valid,
779 estimated_send_rate_bps,
780 estimated_send_rate_valid,
781 estimated_recv_rate_bps,
782 estimated_recv_rate_valid,
783 bytes_sent,
784 bytes_sent_valid,
785 bytes_received,
786 bytes_received_valid,
787 bytes_lost,
788 bytes_lost_valid,
789 packets_sent,
790 packets_sent_valid,
791 packets_received,
792 packets_received_valid,
793 packets_lost,
794 packets_lost_valid,
795 }
796 }
797}
798
799/// Initialize the library with a log level.
800///
801/// This should be called before any other functions.
802/// The log_level is a string: "error", "warn", "info", "debug", "trace"
803///
804/// Returns a zero on success, or a negative code on failure.
805///
806/// # Safety
807/// - The caller must ensure that level is a valid pointer to level_len bytes of data.
808#[unsafe(no_mangle)]
809pub unsafe extern "C" fn moq_log_level(level: *const c_char, level_len: usize) -> i32 {
810 ffi::enter(move || {
811 match unsafe { ffi::parse_str(level, level_len)? } {
812 "" => moq_tokio::Log::default(),
813 level => moq_tokio::Log::new(Level::from_str(level)?),
814 }
815 .init()?;
816
817 Ok(())
818 })
819}
820
821/// Human-readable reason for the most recent failed call on the calling thread.
822///
823/// libmoq functions return only a negative code; this exposes the matching message
824/// (including detail the code can't carry, e.g. which URL failed to parse or why a
825/// decode failed). The string is only meaningful after a call returned a negative
826/// code; check the code first.
827///
828/// Returns a NUL-terminated, UTF-8 pointer valid until the next libmoq call **on the
829/// same thread**, or NULL if no error has been recorded on this thread. Copy it if you
830/// need it to outlive the next call. Errors delivered through status callbacks carry
831/// their code directly; read this from inside the callback to get their reason.
832#[unsafe(no_mangle)]
833pub extern "C" fn moq_error() -> *const c_char {
834 ffi::last_error_ptr()
835}
836
837/// Structured protocol details for the most recent failed call on the calling thread.
838///
839/// When that failure was a session close or stream reset, writes the scope, verbatim
840/// wire code, and recognized kind into `out` and returns 0. Returns a negative code
841/// (and leaves `out` untouched) when the last error was not a protocol failure
842/// (transport, not-found, a bad handle, ...). Do not parse [moq_error] for this.
843///
844/// The values are only meaningful after a call returned a negative code; check the
845/// code first. Same lifetime as [moq_error]: overwritten by the next libmoq call on
846/// this thread. Errors delivered through status callbacks are recorded before the
847/// callback runs, so read this from inside the callback.
848///
849/// # Safety
850/// - The caller must ensure that `out` is a valid pointer to a [moq_protocol_error].
851#[unsafe(no_mangle)]
852pub unsafe extern "C" fn moq_error_protocol(out: *mut moq_protocol_error) -> i32 {
853 // Do not go through `enter`: a miss must not overwrite the last error we are inspecting.
854 if out.is_null() {
855 return Error::InvalidPointer.code();
856 }
857 if ffi::last_protocol(unsafe { &mut *out }) {
858 0
859 } else {
860 Error::NotFound.code()
861 }
862}
863
864/// The protocol version names this build offers by default, spelled the way
865/// [moq_client_config]'s `versions` expects. Built once; the slices are valid for the life of
866/// the process.
867static VERSION_NAMES: std::sync::LazyLock<Vec<String>> =
868 std::sync::LazyLock::new(|| moq_net::Versions::all().iter().map(|v| v.to_string()).collect());
869
870/// List the protocol versions offered during the handshake by default.
871///
872/// Writes up to `count` names into `dst` and returns the total number available, which
873/// may be larger than `count`. Pass a NULL `dst` with a zero `count` to size the array
874/// first. Each name borrows a static string valid for the life of the process, so a
875/// caller building a menu can hold them indefinitely.
876///
877/// Returns the total count on success, or a negative code on failure.
878///
879/// # Safety
880/// - The caller must ensure that `dst` is either NULL with a zero `count`, or a valid
881/// pointer to `count` writable [moq_string] values.
882#[unsafe(no_mangle)]
883pub unsafe extern "C" fn moq_versions(dst: *mut moq_string, count: usize) -> i32 {
884 ffi::enter(move || {
885 if !dst.is_null() {
886 let dst = unsafe { std::slice::from_raw_parts_mut(dst, count) };
887 for (slot, name) in dst.iter_mut().zip(VERSION_NAMES.iter()) {
888 slot.data = name.as_ptr().cast::<c_char>();
889 slot.len = name.len();
890 }
891 } else if count != 0 {
892 return Err(Error::InvalidPointer);
893 }
894
895 Ok(VERSION_NAMES.len())
896 })
897}
898
899/// Whether this build can capture qlog traces.
900///
901/// Capture is compile-time optional. [moq_client_config]'s `quic_qlog` accepts a directory
902/// either way, but dialing fails when the support is absent, so a caller offering the
903/// knob should hide it rather than surface an option that cannot work.
904#[unsafe(no_mangle)]
905pub extern "C" fn moq_qlog_supported() -> bool {
906 moq_tokio::qlog_supported()
907}
908
909/// A duration as microseconds, saturating rather than wrapping.
910fn micros(duration: std::time::Duration) -> u64 {
911 duration.as_micros().min(u64::MAX as u128) as u64
912}
913
914/// Settings for [moq_session_connect], or NULL to dial with the defaults.
915///
916/// Zero it (`memset`, or a `{0}` initializer) and set only what you need: a
917/// zeroed struct means the defaults throughout. That is why the knobs whose
918/// default is not zero carry a `has_*` flag rather than being read directly. The
919/// WebSocket fallback is on by default and the reconnect backoff starts at one
920/// second, so a caller who never touched them would otherwise silently turn them
921/// off.
922///
923/// New settings are appended to the end of this struct, and a zeroed one keeps
924/// the previous behavior, so adding one does not disturb existing callers.
925#[repr(C)]
926#[allow(non_camel_case_types)]
927pub struct moq_client_config {
928 /// Protocol versions to offer during the handshake, most preferred first.
929 /// NULL/0 offers everything this build supports. Names are spelled the way
930 /// the CLI spells them (`moq-lite-05`, `moq-transport-22`); [moq_versions]
931 /// lists what is on offer.
932 pub versions: *const moq_string,
933 pub versions_len: usize,
934
935 /// Local socket address to bind, or NULL for the wildcard address.
936 pub bind: *const c_char,
937 pub bind_len: usize,
938
939 /// How long a dial may take before it gives up.
940 pub connect_timeout_us: u64,
941 pub has_connect_timeout: bool,
942
943 /// Happy Eyeballs: how long before the next address is also dialed.
944 pub failover_delay_us: u64,
945 pub has_failover_delay: bool,
946
947 /// Happy Eyeballs: how long the first family waits for the AAAA answer.
948 pub resolution_delay_us: u64,
949 pub has_resolution_delay: bool,
950
951 /// Whether the WebSocket fallback may be raced, for a UDP-blocked network.
952 /// Enabled unless you turn it off, hence the flag.
953 pub websocket_enabled: bool,
954 pub has_websocket_enabled: bool,
955
956 /// How long QUIC gets before the WebSocket fallback is also dialed.
957 pub websocket_delay_us: u64,
958 pub has_websocket_delay: bool,
959
960 /// Accept any certificate. Development only: prefer `tls_fingerprints`,
961 /// and pairing this with a fingerprint or a root is rejected at dial.
962 pub tls_disable_verify: bool,
963
964 /// Whether to trust the platform root store. Its default depends on the
965 /// backend, so it needs the flag to distinguish "off" from "unset".
966 pub tls_system_roots: bool,
967 pub has_tls_system_roots: bool,
968
969 /// Extra root certificate paths to trust.
970 pub tls_roots: *const moq_string,
971 pub tls_roots_len: usize,
972
973 /// SHA-256 certificate fingerprints to pin, hex encoded. The native
974 /// equivalent of the browser's `serverCertificateHashes`.
975 pub tls_fingerprints: *const moq_string,
976 pub tls_fingerprints_len: usize,
977
978 /// SNI override, or NULL to use the host from the URL.
979 pub tls_host_name: *const c_char,
980 pub tls_host_name_len: usize,
981
982 /// Client certificate and key paths for mTLS, or NULL for none.
983 pub tls_cert: *const c_char,
984 pub tls_cert_len: usize,
985 pub tls_key: *const c_char,
986 pub tls_key_len: usize,
987
988 /// Reconnect pacing. Each must leave a non-zero delay or retrying would
989 /// spin, which is rejected at dial.
990 pub backoff_initial_us: u64,
991 pub has_backoff_initial: bool,
992 pub backoff_multiplier: u32,
993 pub has_backoff_multiplier: bool,
994 pub backoff_max_us: u64,
995 pub has_backoff_max: bool,
996 /// How long reconnection keeps trying before giving up for good.
997 pub backoff_timeout_us: u64,
998 pub has_backoff_timeout: bool,
999
1000 /// QUIC transport tuning, all ignored by the WebSocket fallback.
1001 pub quic_max_streams: u64,
1002 pub has_quic_max_streams: bool,
1003 pub quic_idle_timeout_us: u64,
1004 pub has_quic_idle_timeout: bool,
1005 pub quic_keep_alive_us: u64,
1006 pub has_quic_keep_alive: bool,
1007 /// Generic segmentation offload and path MTU discovery. Both default to the
1008 /// backend's choice, so both need their flag.
1009 pub quic_gso: bool,
1010 pub has_quic_gso: bool,
1011 pub quic_mtu_discovery: bool,
1012 pub has_quic_mtu_discovery: bool,
1013
1014 /// Congestion control family name, or NULL for the backend's choice.
1015 pub quic_congestion_control: *const c_char,
1016 pub quic_congestion_control_len: usize,
1017
1018 /// Directory to write qlog traces into, or NULL for none. Capture is
1019 /// compile-time optional; see [moq_qlog_supported].
1020 pub quic_qlog: *const c_char,
1021 pub quic_qlog_len: usize,
1022}
1023
1024/// The settings [moq_session_connect] dials with when given NULL.
1025///
1026/// Behaviorally the same as a zeroed struct, so this is for display rather than
1027/// for dialing: a settings UI can show the real numbers instead of hardcoding
1028/// ones that go stale when a default is retuned. The knobs whose default depends
1029/// on the backend (GSO, path MTU discovery, congestion control, the TLS root
1030/// store) come back with their `has_*` flag false, since there is no single value
1031/// to report.
1032///
1033/// Returned by value because there is nothing to fail: no handle to look up and
1034/// no pointer to reject. Prefer a zeroed struct when you only mean to set a knob
1035/// or two, and this when you want to read the numbers.
1036#[unsafe(no_mangle)]
1037pub extern "C" fn moq_client_defaults() -> moq_client_config {
1038 // SAFETY: every field is a scalar or a raw pointer, so all-zero is a valid
1039 // value, and it is the one that means "unset" throughout.
1040 let mut dst: moq_client_config = unsafe { std::mem::zeroed() };
1041
1042 // A panic here would have no way to report itself, so fall back to the zeroed
1043 // struct: it is what "the defaults" means to a dial anyway, and only the
1044 // reported numbers would be wrong.
1045 let filled = std::panic::catch_unwind(|| {
1046 let mut dst: moq_client_config = unsafe { std::mem::zeroed() };
1047 let config = crate::client::Config::default();
1048
1049 let connect = config.connect.resolve();
1050 dst.connect_timeout_us = micros(connect.timeout);
1051 dst.has_connect_timeout = true;
1052 dst.failover_delay_us = micros(connect.race);
1053 dst.has_failover_delay = true;
1054 dst.resolution_delay_us = micros(connect.resolution_delay);
1055 dst.has_resolution_delay = true;
1056
1057 let websocket = config.connect.websocket.resolve();
1058 dst.websocket_enabled = websocket.enabled;
1059 dst.has_websocket_enabled = true;
1060 dst.websocket_delay_us = micros(websocket.delay);
1061 dst.has_websocket_delay = true;
1062
1063 dst.backoff_initial_us = micros(config.connect.backoff.initial);
1064 dst.has_backoff_initial = true;
1065 dst.backoff_multiplier = config.connect.backoff.multiplier;
1066 dst.has_backoff_multiplier = true;
1067 dst.backoff_max_us = micros(config.connect.backoff.max);
1068 dst.has_backoff_max = true;
1069 dst.backoff_timeout_us = micros(config.connect.backoff.timeout);
1070 dst.has_backoff_timeout = true;
1071
1072 let quic = config.quic.resolve();
1073 dst.quic_max_streams = quic.max_streams;
1074 dst.has_quic_max_streams = true;
1075 dst.quic_idle_timeout_us = micros(quic.idle_timeout);
1076 dst.has_quic_idle_timeout = true;
1077 if let Some(keep_alive) = quic.keep_alive {
1078 dst.quic_keep_alive_us = micros(keep_alive);
1079 dst.has_quic_keep_alive = true;
1080 }
1081
1082 dst
1083 });
1084
1085 if let Ok(value) = filled {
1086 dst = value;
1087 }
1088
1089 dst
1090}
1091
1092/// Resolve handles under the global lock, prepare the client without it, then insert
1093/// the ready session under a short second lock.
1094unsafe fn connect_session(
1095 url: *const c_char,
1096 url_len: usize,
1097 config: *const moq_client_config,
1098 origin_publish: u32,
1099 origin_consume: u32,
1100 on_status: ffi::moq_status_callback,
1101 user_data: *mut c_void,
1102) -> Result<crate::Id, Error> {
1103 let url = ffi::parse_url(url, url_len)?;
1104 let origin_publish = ffi::parse_id_optional(origin_publish)?;
1105 let origin_consume = ffi::parse_id_optional(origin_consume)?;
1106
1107 // Parse before taking the lock: it validates, and a rejected value should not
1108 // have blocked every other call while it was being read.
1109 let config = unsafe { crate::parse_client(config.as_ref())? };
1110
1111 let (publish, consume) = {
1112 let state = State::lock();
1113 let publish = origin_publish.map(|id| state.origin.get(id)).transpose()?.cloned();
1114 let consume = origin_consume.map(|id| state.origin.get(id)).transpose()?.cloned();
1115 (publish, consume)
1116 };
1117
1118 let callback = unsafe { ffi::OnStatus::new(user_data, on_status)? };
1119 let request = Connect {
1120 config,
1121 url,
1122 publish,
1123 consume,
1124 callback,
1125 }
1126 .prepare()?;
1127
1128 State::lock().session.connect(request)
1129}
1130
1131/// Start establishing a connection to a MoQ server.
1132///
1133/// Takes origin handles, which are used for publishing and consuming broadcasts respectively.
1134/// - Any broadcasts in `origin_publish` will be announced to the server.
1135/// - Any broadcasts announced by the server will be available in `origin_consume`.
1136/// - If an origin handle is 0, that functionality is completely disabled.
1137///
1138/// This may be called multiple times to connect to different servers.
1139/// Origins can be shared across sessions, useful for fanout or relaying.
1140///
1141/// Pass NULL for `config` to dial with the defaults. Fill in a
1142/// [moq_client_config] to pin a protocol version, adjust TLS trust, or tune the
1143/// transport; it is read during the call and not retained, so the same one can
1144/// dial any number of sessions.
1145///
1146/// Returns a non-zero handle to the session on success, or a negative code on (immediate) failure.
1147/// You should call [moq_session_close], even on error, to free up resources.
1148///
1149/// The session reconnects automatically with exponential backoff if the connection drops.
1150/// Published broadcasts are re-announced and consumers re-subscribed on each reconnect,
1151/// since the origins outlive the underlying connection.
1152///
1153/// `on_status` reports the session lifecycle through its status code:
1154/// - `> 0` on every (re)connect, carrying the connection epoch (`1` = first connect,
1155/// `2` = first reconnect, and so on), so a reconnect is distinguishable from the
1156/// initial connect. May fire repeatedly. Transient disconnects are not reported.
1157/// - `0` when the session is closed cleanly via [moq_session_close] (terminal).
1158/// - a negative error code if reconnection permanently gives up, e.g. the backoff
1159/// timeout is exceeded (terminal).
1160///
1161/// After a terminal (`<= 0`) status, `on_status` is never called again and `user_data`
1162/// is never touched again, so that final callback is the point to release `user_data`.
1163/// The terminal `0` fires even after [moq_session_close], so do not free `user_data` on
1164/// the close call itself.
1165///
1166/// # Safety
1167/// - The caller must ensure that url is a valid pointer to url_len bytes of data.
1168/// - `config` must be NULL, or an aligned, readable [moq_client_config]. Every
1169/// non-NULL pointer inside it must be valid for its paired length, and all of
1170/// them must stay alive for the duration of this call: the config is read
1171/// here, not copied by whoever filled it in.
1172/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_status` callback.
1173#[unsafe(no_mangle)]
1174pub unsafe extern "C" fn moq_session_connect(
1175 url: *const c_char,
1176 url_len: usize,
1177 config: *const moq_client_config,
1178 origin_publish: u32,
1179 origin_consume: u32,
1180 on_status: ffi::moq_status_callback,
1181 user_data: *mut c_void,
1182) -> i32 {
1183 ffi::enter(move || unsafe {
1184 connect_session(
1185 url,
1186 url_len,
1187 config,
1188 origin_publish,
1189 origin_consume,
1190 on_status,
1191 user_data,
1192 )
1193 })
1194}
1195
1196/// Request that a session shut down.
1197///
1198/// Returns immediately: zero on success, or a negative code if the session is
1199/// unknown or already closing. Does NOT free `user_data`. The
1200/// [moq_session_connect] `on_status` callback still fires once more with a
1201/// terminal `0` (or a negative error), and that final callback is where
1202/// `user_data` should be released. Safe to call from any thread, including from
1203/// within `on_status`.
1204#[unsafe(no_mangle)]
1205pub extern "C" fn moq_session_close(session: u32) -> i32 {
1206 ffi::enter(move || {
1207 let session = ffi::parse_id(session)?;
1208 State::lock().session.close(session)
1209 })
1210}
1211
1212/// Snapshot the current connection statistics for a session.
1213///
1214/// Fills `dst` with a point-in-time view of the underlying QUIC/WebTransport connection
1215/// (RTT, bandwidth estimates, byte/packet counters). Each metric carries a `*_valid` flag
1216/// since availability depends on the transport backend; see [moq_connection_stats].
1217///
1218/// Returns zero on success, or a negative code on failure: the session handle is unknown, or
1219/// the session is currently reconnecting and has no live connection (in which case `dst` is
1220/// left untouched). Safe to call repeatedly to poll stats over the life of the session.
1221///
1222/// # Safety
1223/// - The caller must ensure that `dst` is a valid pointer to a [moq_connection_stats] struct.
1224#[unsafe(no_mangle)]
1225pub unsafe extern "C" fn moq_session_stats(session: u32, dst: *mut moq_connection_stats) -> i32 {
1226 ffi::enter(move || {
1227 let session = ffi::parse_id(session)?;
1228 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1229 let stats = State::lock().session.stats(session)?;
1230 *dst = moq_connection_stats::from(&stats);
1231 Ok(())
1232 })
1233}
1234
1235/// Snapshot statistics and the negotiated protocol from the same live connection.
1236///
1237/// Returns zero on success, or a negative code when the handle is unknown or offline
1238/// between reconnects. On failure, `dst` is untouched. The protocol string points at
1239/// static storage valid for the process lifetime and must not be freed.
1240///
1241/// # Safety
1242/// - `dst` must point at a writable [moq_connection_snapshot] struct.
1243#[unsafe(no_mangle)]
1244pub unsafe extern "C" fn moq_session_snapshot(session: u32, dst: *mut moq_connection_snapshot) -> i32 {
1245 ffi::enter(move || {
1246 let session = ffi::parse_id(session)?;
1247 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1248 let snapshot = State::lock().session.snapshot(session)?;
1249 let name = snapshot.version.as_str();
1250 *dst = moq_connection_snapshot {
1251 stats: moq_connection_stats::from(&snapshot.stats),
1252 protocol: moq_string {
1253 data: name.as_ptr().cast::<c_char>(),
1254 len: name.len(),
1255 },
1256 };
1257 Ok(())
1258 })
1259}
1260
1261/// Create an origin for publishing broadcasts.
1262///
1263/// Origins contain any number of broadcasts addressed by path.
1264/// The same broadcast can be published to multiple origins under different paths.
1265///
1266/// [moq_origin_announced] can be used to discover broadcasts published to this origin.
1267/// This is extremely useful for discovering what is available on the server to [moq_origin_request].
1268///
1269/// Returns a non-zero handle to the origin on success.
1270#[unsafe(no_mangle)]
1271pub extern "C" fn moq_origin_create() -> i32 {
1272 ffi::enter(move || State::lock().origin.create())
1273}
1274
1275/// Create a broadcast at `path` on an origin, for publishing media tracks.
1276///
1277/// The broadcast appears on this origin's local announcement streams immediately.
1278/// Fill it with the `moq_publish_*` functions, then advertise it to peers with
1279/// [moq_publish_announce] after populating. [moq_publish_finish] unpublishes
1280/// immediately.
1281///
1282/// Returns a non-zero broadcast handle on success, or a negative code on failure.
1283///
1284/// # Safety
1285/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
1286#[unsafe(no_mangle)]
1287pub unsafe extern "C" fn moq_origin_create_broadcast(origin: u32, path: *const c_char, path_len: usize) -> i32 {
1288 ffi::enter(move || {
1289 let origin = ffi::parse_id(origin)?;
1290 let path = unsafe { ffi::parse_str(path, path_len)? };
1291
1292 let mut state = State::lock();
1293 let broadcast = state.origin.create_broadcast(origin, path)?;
1294 state.publish.create(broadcast)
1295 })
1296}
1297
1298/// Advertise `prefix` and serve the requests beneath it.
1299///
1300/// A route claims `prefix` and every path beneath it (the empty prefix claims
1301/// every path). A service that only serves some of them advertises the
1302/// covering prefix and rejects the rest as they are requested. `on_request` is
1303/// required: a NULL callback is refused before the route is advertised. It is
1304/// invoked with a positive request handle for each
1305/// pending broadcast, then exactly once more with a terminal code: `0` (stopped
1306/// cleanly, including after [moq_origin_dynamic_cancel]) or a negative error.
1307/// After the terminal (`<= 0`) callback, `user_data` is never touched again.
1308///
1309/// Returns a non-zero handle on success, or a negative code on failure.
1310///
1311/// # Safety
1312/// - The caller must ensure that prefix is a valid pointer to prefix_len bytes of data.
1313/// - `route` may be NULL, or must point at a readable [moq_route].
1314/// - `on_request` must be non-NULL; a missing callback is refused before the route is advertised.
1315/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_request` callback.
1316#[unsafe(no_mangle)]
1317pub unsafe extern "C" fn moq_origin_dynamic(
1318 origin: u32,
1319 prefix: *const c_char,
1320 prefix_len: usize,
1321 route: *const moq_route,
1322 on_request: ffi::moq_status_callback,
1323 user_data: *mut c_void,
1324) -> i32 {
1325 ffi::enter(move || {
1326 let origin = ffi::parse_id(origin)?;
1327 let prefix = unsafe { ffi::parse_str(prefix, prefix_len)? };
1328 let route = unsafe { parse_route(route)? };
1329 let on_request = unsafe { ffi::OnStatus::new(user_data, on_request)? };
1330 State::lock().origin.dynamic(origin, prefix, route, on_request)
1331 })
1332}
1333
1334/// Re-price a served route in place. The prefix cannot change.
1335///
1336/// Returns a zero on success, or a negative code on failure.
1337///
1338/// # Safety
1339/// - `route` may be NULL, or must point at a readable [moq_route].
1340#[unsafe(no_mangle)]
1341pub unsafe extern "C" fn moq_origin_dynamic_update(dynamic: u32, route: *const moq_route) -> i32 {
1342 ffi::enter(move || {
1343 let dynamic = ffi::parse_id(dynamic)?;
1344 let route = unsafe { parse_route(route)? };
1345 State::lock().origin.dynamic_update(dynamic, route)
1346 })
1347}
1348
1349/// Stop serving and retract the route.
1350///
1351/// Returns immediately: zero on success, or a negative code if already closed.
1352/// The [moq_origin_dynamic] `on_request` callback still fires once more with a
1353/// terminal `0` (or a negative error), and that final callback is where
1354/// `user_data` should be released.
1355#[unsafe(no_mangle)]
1356pub extern "C" fn moq_origin_dynamic_cancel(dynamic: u32) -> i32 {
1357 ffi::enter(move || {
1358 let dynamic = ffi::parse_id(dynamic)?;
1359 State::lock().origin.dynamic_close(dynamic)
1360 })
1361}
1362
1363/// The path of a broadcast request delivered to a [moq_origin_dynamic] callback.
1364///
1365/// The destination borrows the request's storage: copy it out before accept,
1366/// reject, or [moq_broadcast_request_free].
1367///
1368/// Returns a zero on success, or a negative code on failure.
1369///
1370/// # Safety
1371/// - `dst` must point at a writable [moq_string].
1372#[unsafe(no_mangle)]
1373pub unsafe extern "C" fn moq_broadcast_request_path(request: u32, dst: *mut moq_string) -> i32 {
1374 ffi::enter(move || {
1375 let request = ffi::parse_id(request)?;
1376 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1377 State::lock().origin.broadcast_request_path(request, dst)
1378 })
1379}
1380
1381/// Accept a broadcast request with an unannounced broadcast producer.
1382///
1383/// Consumes the request handle. Returns a zero on success, or a negative code
1384/// on failure.
1385#[unsafe(no_mangle)]
1386pub extern "C" fn moq_broadcast_request_accept(request: u32, broadcast: u32) -> i32 {
1387 ffi::enter(move || {
1388 let request = ffi::parse_id(request)?;
1389 let broadcast = ffi::parse_id(broadcast)?;
1390 let mut state = State::lock();
1391 let pending = state.origin.broadcast_request_take(request)?;
1392 let consumer = state.publish.producer(broadcast)?.consume();
1393 pending.accept(&consumer);
1394 Ok(())
1395 })
1396}
1397
1398/// Reject a broadcast request with an application error code.
1399///
1400/// Consumes the request handle. Returns a zero on success, or a negative code
1401/// on failure.
1402#[unsafe(no_mangle)]
1403pub extern "C" fn moq_broadcast_request_reject(request: u32, error_code: u16) -> i32 {
1404 ffi::enter(move || {
1405 let request = ffi::parse_id(request)?;
1406 let pending = State::lock().origin.broadcast_request_take(request)?;
1407 pending.reject(moq_net::Error::App(error_code));
1408 Ok(())
1409 })
1410}
1411
1412/// Free a broadcast request without accepting or rejecting it.
1413///
1414/// Dropping the request rejects it. Returns a zero on success, or a negative
1415/// code if the handle is unknown.
1416#[unsafe(no_mangle)]
1417pub extern "C" fn moq_broadcast_request_free(request: u32) -> i32 {
1418 ffi::enter(move || {
1419 let request = ffi::parse_id(request)?;
1420 State::lock().origin.broadcast_request_take(request)?;
1421 Ok(())
1422 })
1423}
1424
1425/// Learn about broadcasts matching a pattern scope under an origin.
1426///
1427/// `prefix` is a literal path root. `filter` is a pattern relative to that
1428/// prefix, or NULL for every path beneath it. Empty is a valid exact filter.
1429/// Delivered [moq_announce_update] prefixes remain relative to the origin.
1430///
1431/// `on_announce` is invoked with a positive announced ID for each broadcast,
1432/// then exactly once more with a terminal code: `0` (stopped cleanly) or a
1433/// negative error. After the terminal (`<= 0`) callback, `on_announce` is never
1434/// called again and `user_data` is never touched again, so release `user_data`
1435/// there. The terminal callback fires even after [moq_origin_announced_cancel].
1436///
1437/// - [moq_origin_announced_info] is used to query information about the broadcast.
1438/// - [moq_origin_announced_free] releases each delivered announced ID once read.
1439/// - [moq_origin_announced_cancel] is used to stop receiving announcements.
1440///
1441/// Returns a non-zero handle on success, or a negative code on failure.
1442///
1443/// # Safety
1444/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_announce` callback.
1445#[unsafe(no_mangle)]
1446pub unsafe extern "C" fn moq_origin_announced(
1447 origin: u32,
1448 prefix: *const c_char,
1449 prefix_len: usize,
1450 filter: *const c_char,
1451 filter_len: usize,
1452 on_announce: ffi::moq_status_callback,
1453 user_data: *mut c_void,
1454) -> i32 {
1455 ffi::enter(move || {
1456 let origin = ffi::parse_id(origin)?;
1457 let prefix = unsafe { ffi::parse_str(prefix, prefix_len)? }.to_string();
1458 let filter = if filter.is_null() {
1459 None
1460 } else {
1461 Some(unsafe { ffi::parse_str(filter, filter_len)? }.to_string())
1462 };
1463 let on_announce = unsafe { ffi::OnStatus::new(user_data, on_announce)? };
1464 State::lock().origin.announced(origin, prefix, filter, on_announce)
1465 })
1466}
1467
1468/// Query information about a broadcast discovered by [moq_origin_announced].
1469///
1470/// The destination is filled with the route information. The `prefix`, `captures`,
1471/// and capture string pointers borrow the announcement's storage: copy them out
1472/// before calling [moq_origin_announced_free], which invalidates them.
1473///
1474/// Returns a zero on success, or a negative code on failure.
1475///
1476/// # Safety
1477/// - The caller must ensure that `dst` is a valid pointer to a [moq_announce_update] struct.
1478#[unsafe(no_mangle)]
1479pub unsafe extern "C" fn moq_origin_announced_info(announced: u32, dst: *mut moq_announce_update) -> i32 {
1480 ffi::enter(move || {
1481 let announced = ffi::parse_id(announced)?;
1482 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
1483 State::lock().origin.announced_info(announced, dst)
1484 })
1485}
1486
1487/// Free a single announcement delivered to a [moq_origin_announced] `on_announce` callback.
1488///
1489/// Each announce / unannounce event hands the callback a distinct announcement handle (read
1490/// with [moq_origin_announced_info]); release it here once done to avoid leaking one per event
1491/// over the life of the listener. This is per-announcement and distinct from
1492/// [moq_origin_announced_cancel], which stops the listener itself. After freeing,
1493/// any pointer obtained from [moq_origin_announced_info] for this handle is dangling.
1494///
1495/// Returns zero on success, or a negative code if the handle is unknown.
1496#[unsafe(no_mangle)]
1497pub extern "C" fn moq_origin_announced_free(announced: u32) -> i32 {
1498 ffi::enter(move || {
1499 let announced = ffi::parse_id(announced)?;
1500 State::lock().origin.announced_free(announced)
1501 })
1502}
1503
1504/// Stop receiving announcements for broadcasts published to an origin.
1505///
1506/// Returns immediately: zero on success, or a negative code if already closed.
1507/// Does NOT free `user_data`. The [moq_origin_announced] `on_announce` callback
1508/// still fires once more with a terminal `0` (or a negative error), and that
1509/// final callback is where `user_data` should be released.
1510#[unsafe(no_mangle)]
1511pub extern "C" fn moq_origin_announced_cancel(announced: u32) -> i32 {
1512 ffi::enter(move || {
1513 let announced = ffi::parse_id(announced)?;
1514 State::lock().origin.announced_close(announced)
1515 })
1516}
1517
1518/// Consume a broadcast from an origin by path, waiting until something can serve it.
1519///
1520/// Resolves against future announcements: it waits for the announcement to arrive (e.g. over the
1521/// network) and then delivers the broadcast handle via `on_broadcast`. Use it right after
1522/// [moq_session_connect] to avoid racing announcement gossip. To resolve against only what is
1523/// reachable by exact path now, use [moq_origin_request] instead. A local
1524/// broadcast appears on this origin's cursor when created, before peer advertising.
1525///
1526/// `on_broadcast` is invoked with a positive broadcast handle once announced, then exactly once
1527/// more with a terminal code: `0` (the wait finished, including after
1528/// [moq_origin_announced_broadcast_cancel]) or a negative error. After the terminal (`<= 0`) callback,
1529/// `on_broadcast` is never called again and `user_data` is never touched again, so release
1530/// `user_data` there. The broadcast handle is usable with [moq_consume_catalog] / [moq_consume_track]
1531/// and must be freed separately with [moq_consume_close].
1532///
1533/// Returns a non-zero handle to the wait on success, or a negative code on (immediate) failure.
1534///
1535/// # Safety
1536/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
1537/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_broadcast` callback.
1538#[unsafe(no_mangle)]
1539pub unsafe extern "C" fn moq_origin_announced_broadcast(
1540 origin: u32,
1541 path: *const c_char,
1542 path_len: usize,
1543 on_broadcast: ffi::moq_status_callback,
1544 user_data: *mut c_void,
1545) -> i32 {
1546 ffi::enter(move || {
1547 let origin = ffi::parse_id(origin)?;
1548 let path = unsafe { ffi::parse_str(path, path_len)? }.to_string();
1549 let on_broadcast = unsafe { ffi::OnStatus::new(user_data, on_broadcast)? };
1550 State::lock().origin.consume_announced(origin, path, on_broadcast)
1551 })
1552}
1553
1554/// Abort a wait started by [moq_origin_announced_broadcast].
1555///
1556/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
1557/// `user_data`. The [moq_origin_announced_broadcast] `on_broadcast` callback still fires once more
1558/// with a terminal `0` (or a negative error), and that final callback is where `user_data` should
1559/// be released. Any broadcast handle already delivered is unaffected and must still be freed with
1560/// [moq_consume_close].
1561#[unsafe(no_mangle)]
1562pub extern "C" fn moq_origin_announced_broadcast_cancel(task: u32) -> i32 {
1563 ffi::enter(move || {
1564 let task = ffi::parse_id(task)?;
1565 State::lock().origin.consume_announced_close(task)
1566 })
1567}
1568
1569/// Request a broadcast from an origin by path, resolving as soon as it can be served.
1570///
1571/// Resolves against what is reachable by exact path *now*, where
1572/// [moq_origin_announced_broadcast] waits indefinitely: it returns an existing broadcast at once,
1573/// whether announced or not, and fails when none is reachable. It does NOT wait for a later
1574/// announcement. Serve on-demand paths with [moq_origin_dynamic].
1575///
1576/// `on_broadcast` is invoked with a positive broadcast handle once served, then exactly once more
1577/// with a terminal code: `0` (finished, including after [moq_origin_request_cancel]) or a negative
1578/// error. After the terminal (`<= 0`) callback, `user_data` is never touched again, so release it
1579/// there. The broadcast handle is usable with [moq_consume_catalog] / [moq_consume_track] and must
1580/// be freed separately with [moq_consume_close].
1581///
1582/// Returns a non-zero handle to the request on success, or a negative code on (immediate) failure.
1583///
1584/// # Safety
1585/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
1586/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_broadcast` callback.
1587#[unsafe(no_mangle)]
1588pub unsafe extern "C" fn moq_origin_request(
1589 origin: u32,
1590 path: *const c_char,
1591 path_len: usize,
1592 on_broadcast: ffi::moq_status_callback,
1593 user_data: *mut c_void,
1594) -> i32 {
1595 ffi::enter(move || {
1596 let origin = ffi::parse_id(origin)?;
1597 let path = unsafe { ffi::parse_str(path, path_len)? }.to_string();
1598 let on_broadcast = unsafe { ffi::OnStatus::new(user_data, on_broadcast)? };
1599 State::lock().origin.request(origin, path, on_broadcast)
1600 })
1601}
1602
1603/// Abort a request started by [moq_origin_request].
1604///
1605/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
1606/// `user_data`; the [moq_origin_request] `on_broadcast` callback fires once more with a terminal
1607/// code, which is where `user_data` should be released. Any broadcast handle already delivered is
1608/// unaffected and must still be freed with [moq_consume_close].
1609#[unsafe(no_mangle)]
1610pub extern "C" fn moq_origin_request_cancel(task: u32) -> i32 {
1611 ffi::enter(move || {
1612 let task = ffi::parse_id(task)?;
1613 State::lock().origin.consume_announced_close(task)
1614 })
1615}
1616
1617/// Close an origin and clean up its resources.
1618///
1619/// Returns a zero on success, or a negative code on failure.
1620#[unsafe(no_mangle)]
1621pub extern "C" fn moq_origin_close(origin: u32) -> i32 {
1622 ffi::enter(move || {
1623 let origin = ffi::parse_id(origin)?;
1624 State::lock().origin.close(origin)
1625 })
1626}
1627
1628/// Advertise a broadcast's exact path as a route.
1629///
1630/// Announcing again re-prices the route in place. A NULL `route` uses the default
1631/// (no hops, cost 0). The path remains discoverable locally before and after peer advertising.
1632///
1633/// Returns a zero on success, or a negative code on failure.
1634///
1635/// # Safety
1636/// - `route` may be NULL, or must point at a readable [moq_route].
1637#[unsafe(no_mangle)]
1638pub unsafe extern "C" fn moq_publish_announce(broadcast: u32, route: *const moq_route) -> i32 {
1639 ffi::enter(move || {
1640 let broadcast = ffi::parse_id(broadcast)?;
1641 let route = unsafe { parse_route(route)? };
1642 State::lock().publish.announce(broadcast, route)
1643 })
1644}
1645
1646/// Retract a broadcast's exact-path advertisement, if any.
1647///
1648/// The broadcast stays reachable by exact path. Returns a zero on success, or a
1649/// negative code on failure.
1650#[unsafe(no_mangle)]
1651pub extern "C" fn moq_publish_unannounce(broadcast: u32) -> i32 {
1652 ffi::enter(move || {
1653 let broadcast = ffi::parse_id(broadcast)?;
1654 State::lock().publish.unannounce(broadcast)
1655 })
1656}
1657
1658/// Finish a broadcast and release it, ending its catalog cleanly.
1659///
1660/// Subscribers see a normal end of stream rather than an error, and the origin unpublishes
1661/// the path immediately.
1662///
1663/// Returns a zero on success, or a negative code on failure.
1664#[unsafe(no_mangle)]
1665pub extern "C" fn moq_publish_finish(broadcast: u32) -> i32 {
1666 ffi::enter(move || {
1667 let broadcast = ffi::parse_id(broadcast)?;
1668 State::lock().publish.finish(broadcast)
1669 })
1670}
1671
1672/// Publish one audio codec as a new media track.
1673///
1674/// The track is named after the format (`0.opus`), so a subscriber finds it
1675/// through the catalog rather than by a name you choose.
1676/// [moq_audio_init::init] is required: audio resolves its whole rendition from
1677/// those bytes. Frames written with [moq_publish_media_frame] must be in decode
1678/// order.
1679///
1680/// Returns a non-zero handle to the track on success, or a negative code on failure.
1681///
1682/// # Safety
1683/// - `config` must be NULL, or point to an aligned, readable [moq_audio_init].
1684/// Every non-NULL pointer inside it must be valid for its paired length and
1685/// stay alive for the duration of this call. A NULL config is rejected with an
1686/// ordinary error.
1687#[unsafe(no_mangle)]
1688pub unsafe extern "C" fn moq_publish_audio(broadcast: u32, config: *const moq_audio_init) -> i32 {
1689 ffi::enter(move || {
1690 let broadcast = ffi::parse_id(broadcast)?;
1691 let audio = unsafe { parse_audio_init(config)? };
1692 State::lock().publish.audio(broadcast, audio)
1693 })
1694}
1695
1696/// # Safety
1697/// - As [moq_publish_audio], for `config`.
1698unsafe fn parse_audio_init(config: *const moq_audio_init) -> Result<moq_mux::import::AudioInit, Error> {
1699 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1700 let init = unsafe { ffi::parse_slice(config.init, config.init_len)? };
1701 let label = unsafe { ffi::parse_str_optional(config.label, config.label_len)? };
1702
1703 let mut audio = moq_mux::import::AudioInit::new(audio_format_from_u32(config.format)?, init.to_vec());
1704 audio.label = label.map(str::to_string);
1705 Ok(audio)
1706}
1707
1708/// Publish one video codec as a new media track.
1709///
1710/// Named as in [moq_publish_audio]. [moq_video_init::init] may be NULL for a
1711/// format that resolves in band.
1712///
1713/// Returns a non-zero handle to the track on success, or a negative code on failure.
1714///
1715/// # Safety
1716/// - As [moq_publish_audio], for a [moq_video_init].
1717#[unsafe(no_mangle)]
1718pub unsafe extern "C" fn moq_publish_video(broadcast: u32, config: *const moq_video_init) -> i32 {
1719 ffi::enter(move || {
1720 let broadcast = ffi::parse_id(broadcast)?;
1721 let video = unsafe { parse_video_init(config)? };
1722 State::lock().publish.video(broadcast, video)
1723 })
1724}
1725
1726/// # Safety
1727/// - As [moq_publish_audio], for a [moq_video_init].
1728unsafe fn parse_video_init(config: *const moq_video_init) -> Result<moq_mux::import::VideoInit, Error> {
1729 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1730 let init = unsafe { ffi::parse_slice(config.init, config.init_len)? };
1731 let label = unsafe { ffi::parse_str_optional(config.label, config.label_len)? };
1732
1733 let mut video = moq_mux::import::VideoInit::new(video_format_from_u32(config.format)?, init.to_vec());
1734 video.label = label.map(str::to_string);
1735 video.hint = config.hint.resolve();
1736 Ok(video)
1737}
1738
1739/// Publish a container, which demuxes and publishes its own tracks.
1740///
1741/// Feed it whole chunks with [moq_publish_container_write]. Unlike the codec
1742/// entry points there is no label: a container describes each track it publishes
1743/// from its own metadata.
1744///
1745/// Returns a non-zero handle to the container on success, or a negative code on failure.
1746///
1747/// # Safety
1748/// - As [moq_publish_audio], for a [moq_container_init].
1749#[unsafe(no_mangle)]
1750pub unsafe extern "C" fn moq_publish_container(broadcast: u32, config: *const moq_container_init) -> i32 {
1751 ffi::enter(move || {
1752 let broadcast = ffi::parse_id(broadcast)?;
1753 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1754 let init = unsafe { ffi::parse_slice(config.init, config.init_len)? };
1755
1756 let container = moq_mux::import::ContainerInit::new(container_format_from_u32(config.format)?, init.to_vec());
1757 State::lock().publish.container(broadcast, container)
1758 })
1759}
1760
1761/// Draw a group boundary on a media importer.
1762///
1763/// For a codec track this ends the open group; the next frame written starts a new one. Audio has
1764/// no boundary of its own (every packet is independently decodable), so this is the only thing
1765/// that gives it groups: call it after every frame for one group (one QUIC stream) the relay
1766/// forwards without waiting, or at a segment cadence to align with video for HLS/DASH. Video
1767/// groups at its own keyframes and needs this only to override that.
1768///
1769/// A container has its own [moq_publish_container_cut], since it rolls a group on every track it
1770/// publishes rather than ending one group.
1771///
1772/// Returns a zero on success, or a negative code on failure.
1773#[unsafe(no_mangle)]
1774pub extern "C" fn moq_publish_media_cut(media: u32) -> i32 {
1775 ffi::enter(move || {
1776 let media = ffi::parse_id(media)?;
1777 State::lock().publish.media_cut(media)
1778 })
1779}
1780
1781/// Draw a group boundary and number the next group `sequence`.
1782///
1783/// [moq_publish_media_cut] with an explicit sequence, for a caller whose group numbers have to be
1784/// deterministic: two encoders publishing the same content align per GOP so a consumer can fail
1785/// over between them.
1786///
1787/// Returns a zero on success, or a negative code on failure.
1788#[unsafe(no_mangle)]
1789pub extern "C" fn moq_publish_media_seek(media: u32, sequence: u64) -> i32 {
1790 ffi::enter(move || {
1791 let media = ffi::parse_id(media)?;
1792 State::lock().publish.media_seek(media, sequence)
1793 })
1794}
1795
1796/// Finish a media track, flushing any buffered frames. No more frames can be written.
1797///
1798/// Returns a zero on success, or a negative code on failure.
1799#[unsafe(no_mangle)]
1800pub extern "C" fn moq_publish_media_finish(export: u32) -> i32 {
1801 ffi::enter(move || {
1802 let export = ffi::parse_id(export)?;
1803 State::lock().publish.media_finish(export)
1804 })
1805}
1806
1807/// Watch whether a media track has subscribers, so an encoder runs only while someone watches.
1808///
1809/// `on_demand` fires right away with the current [moq_demand] state, again on every
1810/// change, then exactly once more with a terminal code: `0` (the track ended or the
1811/// watcher was stopped with [moq_publish_demand_cancel]) or a negative error. After the
1812/// terminal (`<= 0`) callback, `user_data` is never touched again. Reporting the current
1813/// state first means a track that went unused before the watcher existed still reports it.
1814///
1815/// A container handle is refused: it publishes several tracks and has no single demand.
1816///
1817/// Returns a non-zero watcher handle on success, or a negative code on failure.
1818///
1819/// # Safety
1820/// - `on_demand` must be non-NULL.
1821/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_demand` callback.
1822#[unsafe(no_mangle)]
1823pub unsafe extern "C" fn moq_publish_media_demand(
1824 media: u32,
1825 on_demand: ffi::moq_status_callback,
1826 user_data: *mut c_void,
1827) -> i32 {
1828 ffi::enter(move || {
1829 let media = ffi::parse_id(media)?;
1830 let on_demand = unsafe { ffi::OnStatus::new(user_data, on_demand)? };
1831 let mut state = State::lock();
1832 let demand = state.publish.media_demand(media)?;
1833 state.publish.demand(demand, on_demand)
1834 })
1835}
1836
1837/// Stop a demand watcher from [moq_publish_track_demand], [moq_publish_media_demand],
1838/// [`crate::moq_encode_video_demand`], or [`crate::moq_encode_audio_demand`].
1839///
1840/// Returns immediately: zero on success, or a negative code if already closed. The
1841/// watcher's `on_demand` callback still fires once more with a terminal `0`, and
1842/// that final callback is where `user_data` should be released.
1843#[unsafe(no_mangle)]
1844pub extern "C" fn moq_publish_demand_cancel(watcher: u32) -> i32 {
1845 ffi::enter(move || {
1846 let watcher = ffi::parse_id(watcher)?;
1847 State::lock().publish.demand_close(watcher)
1848 })
1849}
1850
1851/// Write a whole chunk of container bytes.
1852///
1853/// No timestamp: a container carries its tracks' timing itself, and the importer
1854/// reads it out rather than taking the caller's word for it.
1855///
1856/// Returns zero on success, or a negative code on failure.
1857///
1858/// # Safety
1859/// - The caller must ensure `payload` is valid for `payload_size` bytes.
1860#[unsafe(no_mangle)]
1861pub unsafe extern "C" fn moq_publish_container_write(container: u32, payload: *const u8, payload_size: usize) -> i32 {
1862 ffi::enter(move || {
1863 let container = ffi::parse_id(container)?;
1864 let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
1865 State::lock().publish.container_write(container, payload)
1866 })
1867}
1868
1869/// Declare that the next chunk starts a new segment, rolling a group on every
1870/// track the container publishes.
1871///
1872/// An fMP4 source carrying `styp` atoms declares its own segments, so this is
1873/// only needed when it doesn't. Formats with no segment concept (MKV, TS, FLV)
1874/// ignore it.
1875///
1876/// Returns zero on success, or a negative code on failure.
1877#[unsafe(no_mangle)]
1878pub extern "C" fn moq_publish_container_cut(container: u32) -> i32 {
1879 ffi::enter(move || {
1880 let container = ffi::parse_id(container)?;
1881 State::lock().publish.container_cut(container)
1882 })
1883}
1884
1885/// Start a new segment and number its groups `sequence`.
1886///
1887/// Returns zero on success, or a negative code on failure.
1888#[unsafe(no_mangle)]
1889pub extern "C" fn moq_publish_container_seek(container: u32, sequence: u64) -> i32 {
1890 ffi::enter(move || {
1891 let container = ffi::parse_id(container)?;
1892 State::lock().publish.container_seek(container, sequence)
1893 })
1894}
1895
1896/// Finish every track the container publishes and release the handle.
1897///
1898/// Returns zero on success, or a negative code on failure.
1899#[unsafe(no_mangle)]
1900pub extern "C" fn moq_publish_container_finish(container: u32) -> i32 {
1901 ffi::enter(move || {
1902 let container = ffi::parse_id(container)?;
1903 State::lock().publish.container_finish(container)
1904 })
1905}
1906
1907/// Write data to a track.
1908///
1909/// The encoding of `data` depends on the track `format`.
1910/// The timestamp is in microseconds.
1911///
1912/// Returns a zero on success, or a negative code on failure.
1913///
1914/// # Safety
1915/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
1916#[unsafe(no_mangle)]
1917pub unsafe extern "C" fn moq_publish_media_frame(
1918 media: u32,
1919 payload: *const u8,
1920 payload_size: usize,
1921 timestamp_us: u64,
1922) -> i32 {
1923 ffi::enter(move || {
1924 let media = ffi::parse_id(media)?;
1925 let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
1926 let timestamp = hang::container::Timestamp::from_micros(timestamp_us)?;
1927 State::lock().publish.media_frame(media, payload, timestamp)
1928 })
1929}
1930
1931/// Replace the catalog properties shared by every video rendition.
1932///
1933/// 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.
1934///
1935/// Returns zero on success, or a negative code on failure.
1936///
1937/// # Safety
1938/// - The caller must ensure that `properties` points to a valid [moq_video_properties].
1939#[unsafe(no_mangle)]
1940pub unsafe extern "C" fn moq_publish_video_properties(broadcast: u32, properties: *const moq_video_properties) -> i32 {
1941 ffi::enter(move || {
1942 let broadcast = ffi::parse_id(broadcast)?;
1943 let properties = unsafe { properties.as_ref() }.ok_or(Error::InvalidPointer)?;
1944
1945 let mut value = hang::catalog::VideoProperties::default();
1946 value.display = properties.has_display.then_some(hang::catalog::Display {
1947 width: properties.display_width,
1948 height: properties.display_height,
1949 });
1950 value.rotation = properties.has_rotation.then_some(properties.rotation);
1951 value.flip = properties.has_flip.then_some(properties.flip);
1952
1953 State::lock().publish.video_properties(broadcast, value)
1954 })
1955}
1956
1957/// Add or replace a video rendition in a broadcast's catalog.
1958///
1959/// This is the producer counterpart to [moq_consume_video_config]: instead of
1960/// reading a rendition out of a catalog, it writes one into the catalog of a
1961/// broadcast created with [moq_origin_create_broadcast]. The rendition is keyed by
1962/// `config.name`; calling this again with the same name replaces the rendition
1963/// you declared, so a config can be refined in place. It fails only when a
1964/// [moq_publish_video] track owns the name, since that track publishes and
1965/// retires its own rendition. The updated catalog is published to subscribers
1966/// automatically.
1967///
1968/// The struct fields are read as inputs:
1969/// - `name` / `codec` are required (NOT NULL terminated) string slices.
1970/// - `label` may be NULL to omit the human-readable rendition name.
1971/// - `description` may be NULL to omit it.
1972/// - `coded_width` / `coded_height` may be zero to omit them.
1973/// - `container` describes how the frames written to the track are wrapped. A
1974/// zeroed one declares the legacy container, which is what [moq_publish_video]
1975/// writes; declare CMAF or LOC for a [moq_publish_track] whose frames you
1976/// already encode that way.
1977///
1978/// Returns a zero on success, or a negative code on failure.
1979///
1980/// # Safety
1981/// - The caller must ensure that `config` points to a valid [moq_video_config].
1982/// - The caller must ensure each non-NULL pointer inside `config` is valid for its length.
1983#[unsafe(no_mangle)]
1984pub unsafe extern "C" fn moq_publish_video_config(broadcast: u32, config: *const moq_video_config) -> i32 {
1985 ffi::enter(move || {
1986 let broadcast = ffi::parse_id(broadcast)?;
1987 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
1988
1989 let name = unsafe { ffi::parse_str(config.name, config.name_len)? };
1990 let label = unsafe { ffi::parse_str_optional(config.label, config.label_len)? };
1991 let codec = unsafe { ffi::parse_str(config.codec, config.codec_len)? };
1992 let codec = hang::catalog::VideoCodec::from_str(codec).map_err(Error::Hang)?;
1993
1994 let mut video = hang::catalog::VideoConfig::new(codec);
1995 video.label = label.map(str::to_string);
1996 if !config.description.is_null() {
1997 let description = unsafe { ffi::parse_slice(config.description, config.description_len)? };
1998 video.description = Some(bytes::Bytes::copy_from_slice(description));
1999 }
2000 video.coded_width = (config.coded_width > 0).then_some(config.coded_width);
2001 video.coded_height = (config.coded_height > 0).then_some(config.coded_height);
2002 video.container = unsafe { parse_container(&config.container)? };
2003
2004 State::lock().publish.video_config(broadcast, name, video)
2005 })
2006}
2007
2008/// Add or replace an audio rendition in a broadcast's catalog.
2009///
2010/// This is the producer counterpart to [moq_consume_audio_config]. The rendition
2011/// is keyed by `config.name`, on the same terms as [moq_publish_video_config]:
2012/// a repeat call replaces your own rendition, and a name a [moq_publish_audio]
2013/// track owns is refused. The updated catalog is published to subscribers
2014/// automatically.
2015///
2016/// The struct fields are read as inputs:
2017/// - `name` / `codec` are required (NOT NULL terminated) string slices.
2018/// - `label` may be NULL to omit the human-readable rendition name.
2019/// - `sample_rate` / `channel_count` are required.
2020/// - `description` may be NULL to omit it.
2021/// - `container` describes how the frames written to the track are wrapped, the
2022/// same as for [moq_publish_video_config].
2023///
2024/// Returns a zero on success, or a negative code on failure.
2025///
2026/// # Safety
2027/// - The caller must ensure that `config` points to a valid [moq_audio_config].
2028/// - The caller must ensure each non-NULL pointer inside `config` is valid for its length.
2029#[unsafe(no_mangle)]
2030pub unsafe extern "C" fn moq_publish_audio_config(broadcast: u32, config: *const moq_audio_config) -> i32 {
2031 ffi::enter(move || {
2032 let broadcast = ffi::parse_id(broadcast)?;
2033 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
2034
2035 let name = unsafe { ffi::parse_str(config.name, config.name_len)? };
2036 let label = unsafe { ffi::parse_str_optional(config.label, config.label_len)? };
2037 let codec = unsafe { ffi::parse_str(config.codec, config.codec_len)? };
2038 let codec = hang::catalog::AudioCodec::from_str(codec).map_err(Error::Hang)?;
2039
2040 let mut audio = hang::catalog::AudioConfig::new(codec, config.sample_rate, config.channel_count);
2041 audio.label = label.map(str::to_string);
2042 audio.container = unsafe { parse_container(&config.container)? };
2043 if !config.description.is_null() {
2044 let description = unsafe { ffi::parse_slice(config.description, config.description_len)? };
2045 audio.description = Some(bytes::Bytes::copy_from_slice(description));
2046 }
2047
2048 State::lock().publish.audio_config(broadcast, name, audio)
2049 })
2050}
2051
2052/// Remove a video rendition from a broadcast's catalog by name.
2053///
2054/// Removes a rendition added by [moq_publish_video_config]. Any other name is a
2055/// no-op, including one a [moq_publish_video] track owns, which is retired by
2056/// [moq_publish_media_finish] instead. The updated catalog is published to
2057/// subscribers automatically.
2058///
2059/// Returns a zero on success, or a negative code on failure.
2060///
2061/// # Safety
2062/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
2063#[unsafe(no_mangle)]
2064pub unsafe extern "C" fn moq_publish_video_remove(broadcast: u32, name: *const c_char, name_len: usize) -> i32 {
2065 ffi::enter(move || {
2066 let broadcast = ffi::parse_id(broadcast)?;
2067 let name = unsafe { ffi::parse_str(name, name_len)? };
2068 State::lock().publish.video_remove(broadcast, name)
2069 })
2070}
2071
2072/// Remove an audio rendition from a broadcast's catalog by name.
2073///
2074/// Same rules as [moq_publish_video_remove].
2075///
2076/// Returns a zero on success, or a negative code on failure.
2077///
2078/// # Safety
2079/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
2080#[unsafe(no_mangle)]
2081pub unsafe extern "C" fn moq_publish_audio_remove(broadcast: u32, name: *const c_char, name_len: usize) -> i32 {
2082 ffi::enter(move || {
2083 let broadcast = ffi::parse_id(broadcast)?;
2084 let name = unsafe { ffi::parse_str(name, name_len)? };
2085 State::lock().publish.audio_remove(broadcast, name)
2086 })
2087}
2088
2089/// Set (or replace) a top-level application catalog section by name.
2090///
2091/// This is the producer counterpart to [moq_consume_catalog_section] /
2092/// [moq_consume_catalog_section_at]: it writes an arbitrary top-level JSON key into the
2093/// catalog of a broadcast created with [moq_origin_create_broadcast], beyond the
2094/// `video`/`audio` keys owned by the media pipeline. Calling it again with the
2095/// same name replaces the section. The updated catalog is published to
2096/// subscribers automatically.
2097///
2098/// `json` is a JSON document (object, array, string, ...) as `json_len` bytes of
2099/// UTF-8. Returns a zero on success, or a negative code on failure: invalid JSON
2100/// yields a Json error (-37); a reserved `name` (`video`/`audio`) yields a mux error.
2101///
2102/// # Safety
2103/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
2104/// - The caller must ensure that json is a valid pointer to json_len bytes of data.
2105#[unsafe(no_mangle)]
2106pub unsafe extern "C" fn moq_publish_catalog_section(
2107 broadcast: u32,
2108 name: *const c_char,
2109 name_len: usize,
2110 json: *const c_char,
2111 json_len: usize,
2112) -> i32 {
2113 ffi::enter(move || {
2114 let broadcast = ffi::parse_id(broadcast)?;
2115 let name = unsafe { ffi::parse_str(name, name_len)? };
2116 let json = unsafe { ffi::parse_str(json, json_len)? };
2117 let value: serde_json::Value = serde_json::from_str(json)?;
2118 State::lock().publish.catalog_section_set(broadcast, name, value)
2119 })
2120}
2121
2122/// Remove a top-level application catalog section by name.
2123///
2124/// This is a no-op if no section with that name exists. The updated catalog is
2125/// published to subscribers automatically.
2126///
2127/// Returns a zero on success, or a negative code on failure.
2128///
2129/// # Safety
2130/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
2131#[unsafe(no_mangle)]
2132pub unsafe extern "C" fn moq_publish_catalog_section_remove(
2133 broadcast: u32,
2134 name: *const c_char,
2135 name_len: usize,
2136) -> i32 {
2137 ffi::enter(move || {
2138 let broadcast = ffi::parse_id(broadcast)?;
2139 let name = unsafe { ffi::parse_str(name, name_len)? };
2140 State::lock().publish.catalog_section_remove(broadcast, name)
2141 })
2142}
2143
2144/// Create a raw track on a broadcast for arbitrary byte payloads.
2145///
2146/// Unlike [moq_publish_audio] and [moq_publish_video], this is the bare moq-net primitive: no
2147/// codec, container, or catalog framing. Frames written to it are delivered
2148/// as-is to subscribers using [moq_consume_track]. Use it for non-media tracks
2149/// (control channels, JSON metadata, etc.), or pair it with
2150/// [moq_publish_video_config] / [moq_publish_audio_config] to also describe the
2151/// track in the catalog. Pass NULL for `info` to use moq-net defaults.
2152///
2153/// Returns a non-zero handle to the track on success, or a negative code on failure.
2154///
2155/// # Safety
2156/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
2157/// - The caller must ensure that info is either NULL or a valid pointer to a [moq_track_info] struct.
2158#[unsafe(no_mangle)]
2159pub unsafe extern "C" fn moq_publish_track(
2160 broadcast: u32,
2161 name: *const c_char,
2162 name_len: usize,
2163 info: *const moq_track_info,
2164) -> i32 {
2165 ffi::enter(move || {
2166 let broadcast = ffi::parse_id(broadcast)?;
2167 let name = unsafe { ffi::parse_str(name, name_len)? };
2168 let info = unsafe { parse_track_info(info)? };
2169 State::lock().publish.track(broadcast, name, Some(info))
2170 })
2171}
2172
2173/// Raw track info from an optional C struct, defaulting to a microsecond timescale.
2174///
2175/// # Safety
2176/// - `info` must be NULL or a valid pointer to a [moq_track_info] struct.
2177unsafe fn parse_track_info(info: *const moq_track_info) -> Result<moq_net::track::Info, Error> {
2178 // Default raw tracks to a microsecond timescale even when no info is given.
2179 match unsafe { info.as_ref() } {
2180 Some(info) => moq_net::track::Info::try_from(info),
2181 None => Ok(moq_net::track::Info::default().with_timescale(moq_net::Timescale::MICRO)),
2182 }
2183}
2184
2185/// Append a new group to a raw track, returning a group producer.
2186///
2187/// Groups are delivered independently and each may contain any number of frames
2188/// written via [moq_publish_group_frame]. Sequence numbers auto-increment.
2189///
2190/// Returns a non-zero handle to the group on success, or a negative code on failure.
2191#[unsafe(no_mangle)]
2192pub extern "C" fn moq_publish_track_group(track: u32) -> i32 {
2193 ffi::enter(move || {
2194 let track = ffi::parse_id(track)?;
2195 State::lock().publish.track_group(track)
2196 })
2197}
2198
2199/// Create a raw group with an explicit sequence number.
2200///
2201/// Returns a non-zero group handle on success, or a negative code on failure.
2202#[unsafe(no_mangle)]
2203pub extern "C" fn moq_publish_track_group_at(track: u32, sequence: u64) -> i32 {
2204 ffi::enter(move || {
2205 let track = ffi::parse_id(track)?;
2206 State::lock().publish.track_group_at(track, sequence)
2207 })
2208}
2209
2210/// Write a single-frame group to a raw track with a timestamp.
2211///
2212/// Convenience for the common one-frame-per-group pattern. Equivalent to
2213/// appending a group, writing one frame, and finishing it.
2214/// The timestamp is in microseconds.
2215///
2216/// Returns a zero on success, or a negative code on failure.
2217///
2218/// # Safety
2219/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
2220#[unsafe(no_mangle)]
2221pub unsafe extern "C" fn moq_publish_track_frame(
2222 track: u32,
2223 payload: *const u8,
2224 payload_size: usize,
2225 timestamp_us: u64,
2226) -> i32 {
2227 ffi::enter(move || {
2228 let track = ffi::parse_id(track)?;
2229 let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
2230 let timestamp = moq_net::Timestamp::from_micros(timestamp_us)?;
2231 State::lock().publish.track_frame(track, timestamp, payload)
2232 })
2233}
2234
2235/// Send a best-effort datagram on a raw track created by [moq_publish_track].
2236///
2237/// Takes `payload` then `timestamp_us`, matching [moq_publish_track_frame]. The payload must
2238/// be at most 1200 bytes. On success the datagram's per-track sequence number (shared with the
2239/// group namespace) is written to `out_sequence` when it is non-NULL. Datagrams are
2240/// delivered only on transports and wire versions with a datagram channel; there is no
2241/// group fallback.
2242///
2243/// Returns a zero on success, or a negative code on failure.
2244///
2245/// # Safety
2246/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
2247/// - `out_sequence` must be NULL or a valid pointer to a `uint64_t`.
2248#[unsafe(no_mangle)]
2249pub unsafe extern "C" fn moq_publish_track_datagram(
2250 track: u32,
2251 payload: *const u8,
2252 payload_size: usize,
2253 timestamp_us: u64,
2254 out_sequence: *mut u64,
2255) -> i32 {
2256 ffi::enter(move || {
2257 let track = ffi::parse_id(track)?;
2258 let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
2259 let sequence = State::lock().publish.track_datagram(track, timestamp_us, payload)?;
2260 if let Some(out) = unsafe { out_sequence.as_mut() } {
2261 *out = sequence;
2262 }
2263 Ok(())
2264 })
2265}
2266
2267/// Finish a raw track. No more groups or frames can be written.
2268///
2269/// Returns a zero on success, or a negative code on failure.
2270#[unsafe(no_mangle)]
2271pub extern "C" fn moq_publish_track_finish(track: u32) -> i32 {
2272 ffi::enter(move || {
2273 let track = ffi::parse_id(track)?;
2274 State::lock().publish.track_finish(track)
2275 })
2276}
2277
2278/// Declare a raw track's exclusive final group sequence.
2279///
2280/// Groups below `final_sequence` may still be created. Groups at or above it
2281/// are rejected. The track remains open for groups below the boundary. Call
2282/// [moq_publish_track_finish] after producing the remaining groups.
2283#[unsafe(no_mangle)]
2284pub extern "C" fn moq_publish_track_finish_at(track: u32, final_sequence: u64) -> i32 {
2285 ffi::enter(move || {
2286 let track = ffi::parse_id(track)?;
2287 State::lock().publish.track_finish_at(track, final_sequence)
2288 })
2289}
2290
2291/// Abort a raw track with an application error code.
2292#[unsafe(no_mangle)]
2293pub extern "C" fn moq_publish_track_abort(track: u32, error_code: u16) -> i32 {
2294 ffi::enter(move || {
2295 let track = ffi::parse_id(track)?;
2296 State::lock().publish.track_abort(track, error_code)
2297 })
2298}
2299
2300/// Watch whether a raw track has subscribers. See [moq_publish_media_demand] for the
2301/// callback contract.
2302///
2303/// Returns a non-zero watcher handle on success, or a negative code on failure.
2304///
2305/// # Safety
2306/// - `on_demand` must be non-NULL.
2307/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_demand` callback.
2308#[unsafe(no_mangle)]
2309pub unsafe extern "C" fn moq_publish_track_demand(
2310 track: u32,
2311 on_demand: ffi::moq_status_callback,
2312 user_data: *mut c_void,
2313) -> i32 {
2314 ffi::enter(move || {
2315 let track = ffi::parse_id(track)?;
2316 let on_demand = unsafe { ffi::OnStatus::new(user_data, on_demand)? };
2317 let mut state = State::lock();
2318 let demand = state.publish.track_demand(track)?;
2319 state.publish.demand(demand, on_demand)
2320 })
2321}
2322
2323/// Serve subscriber requests for tracks the broadcast has not declared.
2324///
2325/// Without a live handler a subscription to an unknown track name is refused. While one
2326/// is live, `on_request` is invoked with a positive request handle for each pending
2327/// track, then exactly once more with a terminal code: `0` (the broadcast finished, or
2328/// [moq_publish_dynamic_cancel] was called) or a negative error. After the terminal
2329/// (`<= 0`) callback, `user_data` is never touched again. Answer each request with
2330/// [moq_track_request_accept], [moq_track_request_video], [moq_track_request_audio],
2331/// or [moq_track_request_abort]; the subscriber waits until you do.
2332///
2333/// Returns a non-zero handle on success, or a negative code on failure.
2334///
2335/// # Safety
2336/// - `on_request` must be non-NULL.
2337/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_request` callback.
2338#[unsafe(no_mangle)]
2339pub unsafe extern "C" fn moq_publish_dynamic(
2340 broadcast: u32,
2341 on_request: ffi::moq_status_callback,
2342 user_data: *mut c_void,
2343) -> i32 {
2344 ffi::enter(move || {
2345 let broadcast = ffi::parse_id(broadcast)?;
2346 let on_request = unsafe { ffi::OnStatus::new(user_data, on_request)? };
2347 State::lock().publish.dynamic(broadcast, on_request)
2348 })
2349}
2350
2351/// Serve fetches of groups a raw track no longer has cached.
2352///
2353/// Without a live handler a fetch that misses the cache fails as not found. While one is
2354/// live, `on_group` is invoked with a positive group-request handle for each miss, then
2355/// exactly once more with a terminal code: `0` (the track ended, or
2356/// [moq_publish_dynamic_cancel] was called) or a negative error. After the terminal
2357/// (`<= 0`) callback, `user_data` is never touched again. Cached groups never reach the
2358/// handler. Answer each request with [moq_group_request_accept] or [moq_group_request_abort].
2359///
2360/// Returns a non-zero handle on success, or a negative code on failure.
2361///
2362/// # Safety
2363/// - `on_group` must be non-NULL.
2364/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_group` callback.
2365#[unsafe(no_mangle)]
2366pub unsafe extern "C" fn moq_publish_track_dynamic(
2367 track: u32,
2368 on_group: ffi::moq_status_callback,
2369 user_data: *mut c_void,
2370) -> i32 {
2371 ffi::enter(move || {
2372 let track = ffi::parse_id(track)?;
2373 let on_group = unsafe { ffi::OnStatus::new(user_data, on_group)? };
2374 State::lock().publish.track_dynamic(track, on_group)
2375 })
2376}
2377
2378/// Stop a request handler from [moq_publish_dynamic], [moq_publish_track_dynamic], or
2379/// [moq_track_request_dynamic]. Requests not yet delivered are rejected.
2380///
2381/// Returns immediately: zero on success, or a negative code if already closed. The
2382/// handler's callback still fires once more with a terminal `0`, and that final
2383/// callback is where `user_data` should be released.
2384#[unsafe(no_mangle)]
2385pub extern "C" fn moq_publish_dynamic_cancel(dynamic: u32) -> i32 {
2386 ffi::enter(move || {
2387 let dynamic = ffi::parse_id(dynamic)?;
2388 State::lock().publish.dynamic_close(dynamic)
2389 })
2390}
2391
2392/// The name of a track request delivered to a [moq_publish_dynamic] callback.
2393///
2394/// The destination borrows the request's storage: copy it out before accepting,
2395/// aborting, or freeing the request.
2396///
2397/// Returns a zero on success, or a negative code on failure.
2398///
2399/// # Safety
2400/// - `dst` must point at a writable [moq_string].
2401#[unsafe(no_mangle)]
2402pub unsafe extern "C" fn moq_track_request_name(request: u32, dst: *mut moq_string) -> i32 {
2403 ffi::enter(move || {
2404 let request = ffi::parse_id(request)?;
2405 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2406 State::lock().publish.track_request_name(request, dst)
2407 })
2408}
2409
2410/// Serve fetches of uncached groups on a requested track, before accepting it.
2411///
2412/// A track requested by a fetch has that group pending from birth. Register the
2413/// handler here, before [moq_track_request_accept], so the request survives the
2414/// transition; the callback contract is that of [moq_publish_track_dynamic].
2415///
2416/// Returns a non-zero handle on success, or a negative code on failure.
2417///
2418/// # Safety
2419/// - `on_group` must be non-NULL.
2420/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_group` callback.
2421#[unsafe(no_mangle)]
2422pub unsafe extern "C" fn moq_track_request_dynamic(
2423 request: u32,
2424 on_group: ffi::moq_status_callback,
2425 user_data: *mut c_void,
2426) -> i32 {
2427 ffi::enter(move || {
2428 let request = ffi::parse_id(request)?;
2429 let on_group = unsafe { ffi::OnStatus::new(user_data, on_group)? };
2430 State::lock().publish.track_request_dynamic(request, on_group)
2431 })
2432}
2433
2434/// Accept a track request as a raw track, resolving the waiting subscribers.
2435///
2436/// Consumes the request handle. `info` is as in [moq_publish_track]: NULL for the
2437/// microsecond default. Returns a non-zero track handle usable with every
2438/// `moq_publish_track_*` function, or a negative code on failure.
2439///
2440/// # Safety
2441/// - `info` must be NULL or a valid pointer to a [moq_track_info] struct.
2442#[unsafe(no_mangle)]
2443pub unsafe extern "C" fn moq_track_request_accept(request: u32, info: *const moq_track_info) -> i32 {
2444 ffi::enter(move || {
2445 let request = ffi::parse_id(request)?;
2446 let info = unsafe { parse_track_info(info)? };
2447 State::lock().publish.track_request_accept(request, info)
2448 })
2449}
2450
2451/// Accept a track request as an audio track, the importer picking the timescale.
2452///
2453/// Consumes the request handle. Returns the same kind of media handle as
2454/// [moq_publish_audio], or a negative code on failure.
2455///
2456/// # Safety
2457/// - As [moq_publish_audio], for `config`.
2458#[unsafe(no_mangle)]
2459pub unsafe extern "C" fn moq_track_request_audio(request: u32, config: *const moq_audio_init) -> i32 {
2460 ffi::enter(move || {
2461 let request = ffi::parse_id(request)?;
2462 let audio = unsafe { parse_audio_init(config)? };
2463 State::lock().publish.track_request_audio(request, audio)
2464 })
2465}
2466
2467/// Accept a track request as a video track, the importer picking the timescale.
2468///
2469/// Consumes the request handle. Returns the same kind of media handle as
2470/// [moq_publish_video], or a negative code on failure.
2471///
2472/// # Safety
2473/// - As [moq_publish_audio], for a [moq_video_init].
2474#[unsafe(no_mangle)]
2475pub unsafe extern "C" fn moq_track_request_video(request: u32, config: *const moq_video_init) -> i32 {
2476 ffi::enter(move || {
2477 let request = ffi::parse_id(request)?;
2478 let video = unsafe { parse_video_init(config)? };
2479 State::lock().publish.track_request_video(request, video)
2480 })
2481}
2482
2483/// Reject a track request with an application error code, failing the waiting subscribers.
2484///
2485/// Consumes the request handle. Returns a zero on success, or a negative code on failure.
2486#[unsafe(no_mangle)]
2487pub extern "C" fn moq_track_request_abort(request: u32, error_code: u16) -> i32 {
2488 ffi::enter(move || {
2489 let request = ffi::parse_id(request)?;
2490 State::lock().publish.track_request_abort(request, error_code)
2491 })
2492}
2493
2494/// Free a track request without accepting it, which rejects it.
2495///
2496/// Returns a zero on success, or a negative code if the handle is unknown.
2497#[unsafe(no_mangle)]
2498pub extern "C" fn moq_track_request_free(request: u32) -> i32 {
2499 ffi::enter(move || {
2500 let request = ffi::parse_id(request)?;
2501 State::lock().publish.track_request_free(request)
2502 })
2503}
2504
2505/// The group sequence a group request asks for.
2506///
2507/// Returns a zero on success, or a negative code on failure.
2508///
2509/// # Safety
2510/// - `dst` must point at a writable `uint64_t`.
2511#[unsafe(no_mangle)]
2512pub unsafe extern "C" fn moq_group_request_sequence(request: u32, dst: *mut u64) -> i32 {
2513 ffi::enter(move || {
2514 let request = ffi::parse_id(request)?;
2515 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2516 *dst = State::lock().publish.group_request_info(request)?.0;
2517 Ok(())
2518 })
2519}
2520
2521/// The delivery priority the fetching consumer asked for.
2522///
2523/// Returns a zero on success, or a negative code on failure.
2524///
2525/// # Safety
2526/// - `dst` must point at a writable `uint8_t`.
2527#[unsafe(no_mangle)]
2528pub unsafe extern "C" fn moq_group_request_priority(request: u32, dst: *mut u8) -> i32 {
2529 ffi::enter(move || {
2530 let request = ffi::parse_id(request)?;
2531 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2532 *dst = State::lock().publish.group_request_info(request)?.1;
2533 Ok(())
2534 })
2535}
2536
2537/// The first frame of the group the fetch wants; 0 is the whole group.
2538///
2539/// [moq_group_request_accept] positions the returned producer here, so frames you
2540/// write keep the indices they have in the group rather than restarting at 0. Read
2541/// this to know which frames to fetch from storage.
2542///
2543/// Returns a zero on success, or a negative code on failure.
2544///
2545/// # Safety
2546/// - `dst` must point at a writable `uint64_t`.
2547#[unsafe(no_mangle)]
2548pub unsafe extern "C" fn moq_group_request_frame_start(request: u32, dst: *mut u64) -> i32 {
2549 ffi::enter(move || {
2550 let request = ffi::parse_id(request)?;
2551 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2552 *dst = State::lock().publish.group_request_info(request)?.2;
2553 Ok(())
2554 })
2555}
2556
2557/// Accept a group request, resolving the waiting fetches with the group you then fill.
2558///
2559/// Consumes the request handle. The returned producer starts at
2560/// [moq_group_request_frame_start], so the first frame you write lands at that
2561/// index. Returns a non-zero group handle usable with [moq_publish_group_frame]
2562/// and [moq_publish_group_finish], or a negative code on failure, including when
2563/// the group is already cached.
2564#[unsafe(no_mangle)]
2565pub extern "C" fn moq_group_request_accept(request: u32) -> i32 {
2566 ffi::enter(move || {
2567 let request = ffi::parse_id(request)?;
2568 State::lock().publish.group_request_accept(request)
2569 })
2570}
2571
2572/// Reject a group request with an application error code, failing the waiting fetches.
2573///
2574/// Consumes the request handle. Returns a zero on success, or a negative code on failure.
2575#[unsafe(no_mangle)]
2576pub extern "C" fn moq_group_request_abort(request: u32, error_code: u16) -> i32 {
2577 ffi::enter(move || {
2578 let request = ffi::parse_id(request)?;
2579 State::lock().publish.group_request_abort(request, error_code)
2580 })
2581}
2582
2583/// Free a group request without accepting it, which rejects it.
2584///
2585/// Returns a zero on success, or a negative code if the handle is unknown.
2586#[unsafe(no_mangle)]
2587pub extern "C" fn moq_group_request_free(request: u32) -> i32 {
2588 ffi::enter(move || {
2589 let request = ffi::parse_id(request)?;
2590 State::lock().publish.group_request_free(request)
2591 })
2592}
2593
2594/// Write a frame into a raw group created by [moq_publish_track_group].
2595///
2596/// The timestamp is in microseconds.
2597///
2598/// Returns a zero on success, or a negative code on failure.
2599///
2600/// # Safety
2601/// - The caller must ensure that payload is a valid pointer to payload_size bytes of data.
2602#[unsafe(no_mangle)]
2603pub unsafe extern "C" fn moq_publish_group_frame(
2604 group: u32,
2605 payload: *const u8,
2606 payload_size: usize,
2607 timestamp_us: u64,
2608) -> i32 {
2609 ffi::enter(move || {
2610 let group = ffi::parse_id(group)?;
2611 let payload = unsafe { ffi::parse_slice(payload, payload_size)? };
2612 let timestamp = moq_net::Timestamp::from_micros(timestamp_us)?;
2613 State::lock().publish.group_frame(group, timestamp, payload)
2614 })
2615}
2616
2617/// Finish a raw group. No more frames can be written.
2618///
2619/// Returns a zero on success, or a negative code on failure.
2620#[unsafe(no_mangle)]
2621pub extern "C" fn moq_publish_group_finish(group: u32) -> i32 {
2622 ffi::enter(move || {
2623 let group = ffi::parse_id(group)?;
2624 State::lock().publish.group_finish(group)
2625 })
2626}
2627
2628/// Abort a raw group with an application error code.
2629#[unsafe(no_mangle)]
2630pub extern "C" fn moq_publish_group_abort(group: u32, error_code: u16) -> i32 {
2631 ffi::enter(move || {
2632 let group = ffi::parse_id(group)?;
2633 State::lock().publish.group_abort(group, error_code)
2634 })
2635}
2636
2637/// Create a JSON snapshot track (lossy latest-value) on a broadcast.
2638///
2639/// Values published via [moq_publish_json_snapshot_update] reach subscribers as a single latest
2640/// state; a late joiner only sees the newest. Advertise the track in the catalog with
2641/// [moq_publish_catalog_section] if consumers should discover it.
2642///
2643/// Returns a non-zero handle to the JSON producer on success, or a negative code on failure.
2644///
2645/// # Safety
2646/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
2647#[unsafe(no_mangle)]
2648pub unsafe extern "C" fn moq_publish_json_snapshot(
2649 broadcast: u32,
2650 name: *const c_char,
2651 name_len: usize,
2652 config: *const moq_json_snapshot_config,
2653) -> i32 {
2654 ffi::enter(move || {
2655 let broadcast = ffi::parse_id(broadcast)?;
2656 let name = unsafe { ffi::parse_str(name, name_len)? };
2657 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
2658 let mut producer = moq_json::snapshot::Config::default();
2659 producer.delta_ratio = config.delta_ratio;
2660 producer.compression = if config.compression {
2661 moq_json::Compression::Deflate
2662 } else {
2663 moq_json::Compression::None
2664 };
2665 State::lock().publish.json_snapshot(broadcast, name, producer)
2666 })
2667}
2668
2669/// Publish a new value to a JSON snapshot track. `value` is a UTF-8 JSON document. A no-op if
2670/// unchanged from the previous update.
2671///
2672/// Returns a zero on success, or a negative code on failure.
2673///
2674/// # Safety
2675/// - The caller must ensure `value` is a valid pointer to `value_len` bytes.
2676#[unsafe(no_mangle)]
2677pub unsafe extern "C" fn moq_publish_json_snapshot_update(json: u32, value: *const c_char, value_len: usize) -> i32 {
2678 ffi::enter(move || {
2679 let json = ffi::parse_id(json)?;
2680 let value = unsafe { ffi::parse_slice(value.cast::<u8>(), value_len)? };
2681 let value = serde_json::from_slice(value)?;
2682 State::lock().publish.json_snapshot_update(json, value)
2683 })
2684}
2685
2686/// Finish a JSON snapshot track. No more values can be published.
2687///
2688/// Returns a zero on success, or a negative code on failure.
2689#[unsafe(no_mangle)]
2690pub extern "C" fn moq_publish_json_snapshot_finish(json: u32) -> i32 {
2691 ffi::enter(move || {
2692 let json = ffi::parse_id(json)?;
2693 State::lock().publish.json_snapshot_finish(json)
2694 })
2695}
2696
2697/// Create a JSON stream track (lossless append-log) on a broadcast.
2698///
2699/// Every record appended via [moq_publish_json_stream_append] is preserved and delivered in order.
2700///
2701/// Returns a non-zero handle to the JSON stream producer on success, or a negative code on failure.
2702///
2703/// # Safety
2704/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
2705#[unsafe(no_mangle)]
2706pub unsafe extern "C" fn moq_publish_json_stream(
2707 broadcast: u32,
2708 name: *const c_char,
2709 name_len: usize,
2710 config: *const moq_json_stream_config,
2711) -> i32 {
2712 ffi::enter(move || {
2713 let broadcast = ffi::parse_id(broadcast)?;
2714 let name = unsafe { ffi::parse_str(name, name_len)? };
2715 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
2716 let mut producer = moq_json::stream::Config::default();
2717 if config.compression {
2718 producer.compression = moq_json::Compression::Deflate;
2719 }
2720 State::lock().publish.json_stream(broadcast, name, producer)
2721 })
2722}
2723
2724/// Append one record to a JSON stream track. `value` is a UTF-8 JSON document.
2725///
2726/// Returns a zero on success, or a negative code on failure.
2727///
2728/// # Safety
2729/// - The caller must ensure `value` is a valid pointer to `value_len` bytes.
2730#[unsafe(no_mangle)]
2731pub unsafe extern "C" fn moq_publish_json_stream_append(stream: u32, value: *const c_char, value_len: usize) -> i32 {
2732 ffi::enter(move || {
2733 let stream = ffi::parse_id(stream)?;
2734 let value = unsafe { ffi::parse_slice(value.cast::<u8>(), value_len)? };
2735 let value = serde_json::from_slice(value)?;
2736 State::lock().publish.json_stream_append(stream, value)
2737 })
2738}
2739
2740/// Finish a JSON stream track. No more records can be appended.
2741///
2742/// Returns a zero on success, or a negative code on failure.
2743#[unsafe(no_mangle)]
2744pub extern "C" fn moq_publish_json_stream_finish(stream: u32) -> i32 {
2745 ffi::enter(move || {
2746 let stream = ffi::parse_id(stream)?;
2747 State::lock().publish.json_stream_finish(stream)
2748 })
2749}
2750
2751/// Create a catalog consumer for a broadcast.
2752///
2753/// `on_catalog` is invoked with a positive catalog ID for each catalog update
2754/// (usable to query video/audio track information), then exactly once more with
2755/// a terminal code: `0` (closed cleanly) or a negative error. After the terminal
2756/// (`<= 0`) callback, `on_catalog` is never called again and `user_data` is never
2757/// touched again, so release `user_data` there. The terminal callback fires even
2758/// after [moq_consume_catalog_cancel].
2759///
2760/// Returns a non-zero handle on success, or a negative code on failure.
2761///
2762/// # Safety
2763/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_catalog` callback.
2764#[unsafe(no_mangle)]
2765pub unsafe extern "C" fn moq_consume_catalog(
2766 broadcast: u32,
2767 on_catalog: ffi::moq_status_callback,
2768 user_data: *mut c_void,
2769) -> i32 {
2770 ffi::enter(move || {
2771 let broadcast = ffi::parse_id(broadcast)?;
2772 let on_catalog = unsafe { ffi::OnStatus::new(user_data, on_catalog)? };
2773 State::lock().consume.catalog(broadcast, on_catalog)
2774 })
2775}
2776
2777/// Stop a catalog consumer's background subscription.
2778///
2779/// Returns immediately: zero on success, or a negative code if already closed.
2780/// Does NOT free `user_data`; the [moq_consume_catalog] callback still fires once
2781/// more with a terminal `0` (or a negative error), which is where `user_data`
2782/// should be released. Catalog snapshots previously delivered via the callback
2783/// remain valid until freed with [moq_consume_catalog_free].
2784#[unsafe(no_mangle)]
2785pub extern "C" fn moq_consume_catalog_cancel(catalog: u32) -> i32 {
2786 ffi::enter(move || {
2787 let catalog = ffi::parse_id(catalog)?;
2788 State::lock().consume.catalog_close(catalog)
2789 })
2790}
2791
2792/// Free a catalog snapshot received via the [moq_consume_catalog] callback.
2793///
2794/// This releases the snapshot and invalidates any borrowed references (e.g. pointers
2795/// returned by [moq_consume_video_config] or [moq_consume_audio_config]).
2796///
2797/// Returns a zero on success, or a negative code on failure.
2798#[unsafe(no_mangle)]
2799pub extern "C" fn moq_consume_catalog_free(catalog: u32) -> i32 {
2800 ffi::enter(move || {
2801 let catalog = ffi::parse_id(catalog)?;
2802 State::lock().consume.catalog_free(catalog)
2803 })
2804}
2805
2806/// Query information about a video track in a catalog.
2807///
2808/// The destination is filled with the video track information. `dst->container`
2809/// says how the track's frames are wrapped; skip a rendition whose kind is
2810/// `MOQ_CONTAINER_KIND_UNKNOWN`, since this build cannot parse it.
2811///
2812/// Returns a zero on success, or a negative code on failure.
2813///
2814/// # Safety
2815/// - The caller must ensure that `dst` is a valid pointer to a [moq_video_config] struct.
2816/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
2817#[unsafe(no_mangle)]
2818pub unsafe extern "C" fn moq_consume_video_config(catalog: u32, index: u32, dst: *mut moq_video_config) -> i32 {
2819 ffi::enter(move || {
2820 let catalog = ffi::parse_id(catalog)?;
2821 let index = index as usize;
2822 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2823 State::lock().consume.video_config(catalog, index, dst)
2824 })
2825}
2826
2827/// Query whether the publisher recommends temporarily avoiding a video rendition.
2828///
2829/// The track remains available. A false value also covers catalogs that omit the
2830/// optional field.
2831///
2832/// Returns zero on success, or a negative code on failure.
2833///
2834/// # Safety
2835/// - The caller must ensure that `dst` points to properly aligned, writable storage for a `bool`.
2836#[unsafe(no_mangle)]
2837pub unsafe extern "C" fn moq_consume_video_stalled(catalog: u32, index: u32, dst: *mut bool) -> i32 {
2838 ffi::enter(move || {
2839 let catalog = ffi::parse_id(catalog)?;
2840 if dst.is_null() {
2841 return Err(Error::InvalidPointer);
2842 }
2843
2844 let stalled = State::lock().consume.video_stalled(catalog, index as usize)?;
2845 unsafe { dst.write(stalled) };
2846 Ok(())
2847 })
2848}
2849
2850/// Query the catalog properties shared by every video rendition.
2851///
2852/// The destination is filled by value and remains valid after the catalog snapshot is freed.
2853/// Inspect each `has_*` flag before reading its value.
2854///
2855/// Returns zero on success, or a negative code on failure.
2856///
2857/// # Safety
2858/// - The caller must ensure that `dst` points to a valid [moq_video_properties].
2859#[unsafe(no_mangle)]
2860pub unsafe extern "C" fn moq_consume_video_properties(catalog: u32, dst: *mut moq_video_properties) -> i32 {
2861 ffi::enter(move || {
2862 let catalog = ffi::parse_id(catalog)?;
2863 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2864 State::lock().consume.video_properties(catalog, dst)
2865 })
2866}
2867
2868/// Query information about an audio track in a catalog.
2869///
2870/// The destination is filled with the audio track information. `dst->container`
2871/// says how the track's frames are wrapped; skip a rendition whose kind is
2872/// `MOQ_CONTAINER_KIND_UNKNOWN`, since this build cannot parse it.
2873///
2874/// Returns a zero on success, or a negative code on failure.
2875///
2876/// # Safety
2877/// - The caller must ensure that `dst` is a valid pointer to a [moq_audio_config] struct.
2878/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
2879#[unsafe(no_mangle)]
2880pub unsafe extern "C" fn moq_consume_audio_config(catalog: u32, index: u32, dst: *mut moq_audio_config) -> i32 {
2881 ffi::enter(move || {
2882 let catalog = ffi::parse_id(catalog)?;
2883 let index = index as usize;
2884 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2885 State::lock().consume.audio_config(catalog, index, dst)
2886 })
2887}
2888
2889/// Number of untyped application catalog sections in a catalog snapshot.
2890///
2891/// These are the top-level catalog keys beyond `video`/`audio`, carried through
2892/// verbatim. Iterate them by index with [moq_consume_catalog_section_at], or look one up
2893/// directly by name with [moq_consume_catalog_section].
2894///
2895/// Returns the count (>= 0) on success, or a negative code on failure.
2896#[unsafe(no_mangle)]
2897pub extern "C" fn moq_consume_catalog_section_count(catalog: u32) -> i32 {
2898 ffi::enter(move || {
2899 let catalog = ffi::parse_id(catalog)?;
2900 State::lock().consume.catalog_section_count(catalog)
2901 })
2902}
2903
2904/// Query an application catalog section by index, keyed by name.
2905///
2906/// Fills `dst` with the section's name and JSON value at `index`, in the range
2907/// `[0, moq_consume_catalog_section_count)`. Both pointers borrow the snapshot's storage
2908/// and stay valid until it is freed with [moq_consume_catalog_free].
2909///
2910/// Returns a zero on success, or a negative code on failure (e.g. `index` out of
2911/// range).
2912///
2913/// # Safety
2914/// - The caller must ensure that `dst` is a valid pointer to a [moq_section] struct.
2915/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
2916#[unsafe(no_mangle)]
2917pub unsafe extern "C" fn moq_consume_catalog_section_at(catalog: u32, index: u32, dst: *mut moq_section) -> i32 {
2918 ffi::enter(move || {
2919 let catalog = ffi::parse_id(catalog)?;
2920 let index = index as usize;
2921 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2922 State::lock().consume.catalog_section_at(catalog, index, dst)
2923 })
2924}
2925
2926/// Look up an application catalog section by name.
2927///
2928/// Fills `dst` with the section's JSON value (the document to parse yourself).
2929/// The pointer borrows the snapshot's storage and stays valid until it is freed
2930/// with [moq_consume_catalog_free].
2931///
2932/// Returns a zero on success, or a negative code on failure: no section with that
2933/// name yields a not-found error.
2934///
2935/// # Safety
2936/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
2937/// - The caller must ensure that `dst` is a valid pointer to a [moq_string] struct.
2938/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called.
2939#[unsafe(no_mangle)]
2940pub unsafe extern "C" fn moq_consume_catalog_section(
2941 catalog: u32,
2942 name: *const c_char,
2943 name_len: usize,
2944 dst: *mut moq_string,
2945) -> i32 {
2946 ffi::enter(move || {
2947 let catalog = ffi::parse_id(catalog)?;
2948 let name = unsafe { ffi::parse_str(name, name_len)? };
2949 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
2950 State::lock().consume.catalog_section_get(catalog, name, dst)
2951 })
2952}
2953
2954/// Consume a video track from a broadcast, delivering frames in order.
2955///
2956/// - `max_age_us` controls the maximum amount of buffering allowed before skipping a GoP.
2957/// - `on_frame` is called with a positive frame ID per frame, then exactly once
2958/// more with a terminal code: `0` (closed cleanly) or a negative error. After
2959/// the terminal (`<= 0`) callback, `on_frame` is never called again and
2960/// `user_data` is never touched again, so release `user_data` there. The
2961/// terminal callback fires even after [moq_consume_video_cancel].
2962///
2963/// Returns a non-zero handle to the track on success, or a negative code on failure.
2964///
2965/// # Safety
2966/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
2967#[unsafe(no_mangle)]
2968pub unsafe extern "C" fn moq_consume_video(
2969 catalog: u32,
2970 index: u32,
2971 max_age_us: u64,
2972 on_frame: ffi::moq_status_callback,
2973 user_data: *mut c_void,
2974) -> i32 {
2975 ffi::enter(move || {
2976 let catalog = ffi::parse_id(catalog)?;
2977 let index = index as usize;
2978 let max_age = std::time::Duration::from_micros(max_age_us);
2979 let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame)? };
2980 State::lock().consume.video(catalog, index, max_age, on_frame)
2981 })
2982}
2983
2984/// Stop a video track consumer's background task.
2985///
2986/// Returns immediately: zero on success, or a negative code if already closed.
2987/// Does NOT free `user_data`; the [moq_consume_video] `on_frame` callback
2988/// still fires once more with a terminal `0` (or a negative error), which is
2989/// where `user_data` should be released.
2990#[unsafe(no_mangle)]
2991pub extern "C" fn moq_consume_video_cancel(track: u32) -> i32 {
2992 ffi::enter(move || {
2993 let track = ffi::parse_id(track)?;
2994 State::lock().consume.track_close(track)
2995 })
2996}
2997
2998/// Consume an audio track from a broadcast, emitting the frames in order.
2999///
3000/// `on_frame` is called with a positive frame ID per frame, then exactly once
3001/// more with a terminal code: `0` (closed cleanly) or a negative error. After
3002/// the terminal (`<= 0`) callback, `on_frame` is never called again and
3003/// `user_data` is never touched again, so release `user_data` there. The
3004/// terminal callback fires even after [moq_consume_audio_cancel].
3005/// The `max_age_us` parameter controls how long to wait before skipping frames.
3006///
3007/// Returns a non-zero handle to the track on success, or a negative code on failure.
3008///
3009/// # Safety
3010/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
3011#[unsafe(no_mangle)]
3012pub unsafe extern "C" fn moq_consume_audio(
3013 catalog: u32,
3014 index: u32,
3015 max_age_us: u64,
3016 on_frame: ffi::moq_status_callback,
3017 user_data: *mut c_void,
3018) -> i32 {
3019 ffi::enter(move || {
3020 let catalog = ffi::parse_id(catalog)?;
3021 let index = index as usize;
3022 let max_age = std::time::Duration::from_micros(max_age_us);
3023 let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame)? };
3024 State::lock().consume.audio(catalog, index, max_age, on_frame)
3025 })
3026}
3027
3028/// Stop an audio track consumer's background task.
3029///
3030/// Returns immediately: zero on success, or a negative code if already closed.
3031/// Does NOT free `user_data`; the [moq_consume_audio] `on_frame` callback
3032/// still fires once more with a terminal `0` (or a negative error), which is
3033/// where `user_data` should be released.
3034#[unsafe(no_mangle)]
3035pub extern "C" fn moq_consume_audio_cancel(track: u32) -> i32 {
3036 ffi::enter(move || {
3037 let track = ffi::parse_id(track)?;
3038 State::lock().consume.track_close(track)
3039 })
3040}
3041
3042/// Get a chunk of a frame's payload.
3043///
3044/// Read the payload of a frame as a single contiguous slice.
3045///
3046/// Frames are not chunked; the entire payload is delivered through `dst.payload` /
3047/// `dst.payload_size` in one call. The pointer is valid until [`moq_consume_frame_free`]
3048/// is called for this frame.
3049///
3050/// Returns a zero on success, or a negative code on failure.
3051///
3052/// # Safety
3053/// - The caller must ensure that `dst` is a valid pointer to a [moq_frame] struct.
3054#[unsafe(no_mangle)]
3055pub unsafe extern "C" fn moq_consume_frame(frame: u32, dst: *mut moq_frame) -> i32 {
3056 ffi::enter(move || {
3057 let frame = ffi::parse_id(frame)?;
3058 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
3059 State::lock().consume.frame(frame, dst)
3060 })
3061}
3062
3063/// Free a decoded frame delivered via a [moq_consume_video] or [moq_consume_audio] callback.
3064///
3065/// Returns a zero on success, or a negative code on failure.
3066#[unsafe(no_mangle)]
3067pub extern "C" fn moq_consume_frame_free(frame: u32) -> i32 {
3068 ffi::enter(move || {
3069 let frame = ffi::parse_id(frame)?;
3070 State::lock().consume.frame_close(frame)
3071 })
3072}
3073
3074/// Close a broadcast consumer and clean up its resources.
3075///
3076/// Returns a zero on success, or a negative code on failure.
3077#[unsafe(no_mangle)]
3078pub extern "C" fn moq_consume_close(consume: u32) -> i32 {
3079 ffi::enter(move || {
3080 let consume = ffi::parse_id(consume)?;
3081 State::lock().consume.close(consume)
3082 })
3083}
3084
3085/// Subscribe to a raw track by name, delivering each frame's payload as-is.
3086///
3087/// This is the counterpart to [moq_publish_track]: no catalog lookup or
3088/// container parsing. `on_frame` is called with a positive raw frame ID for each
3089/// frame in sequence order, then exactly once more with a terminal code: `0`
3090/// (closed cleanly) or a negative error. After the terminal (`<= 0`) callback,
3091/// `on_frame` is never called again and `user_data` is never touched again, so
3092/// release `user_data` there. The terminal callback fires even after
3093/// [moq_consume_track_cancel]. Read each frame with [moq_consume_track_frame] and
3094/// release it with [moq_consume_track_frame_free]. Pass NULL for `subscription`
3095/// to use moq-net defaults.
3096///
3097/// Returns a non-zero handle to the track on success, or a negative code on failure.
3098///
3099/// # Safety
3100/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
3101/// - The caller must ensure that subscription is either NULL or a valid pointer to a [moq_subscription] struct.
3102/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback.
3103#[unsafe(no_mangle)]
3104pub unsafe extern "C" fn moq_consume_track(
3105 broadcast: u32,
3106 name: *const c_char,
3107 name_len: usize,
3108 subscription: *const moq_subscription,
3109 on_frame: ffi::moq_status_callback,
3110 user_data: *mut c_void,
3111) -> i32 {
3112 ffi::enter(move || {
3113 let broadcast = ffi::parse_id(broadcast)?;
3114 let name = unsafe { ffi::parse_str(name, name_len)? };
3115 let subscription = unsafe { subscription.as_ref() }.map(moq_net::track::Subscription::from);
3116 let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame)? };
3117 State::lock().consume.raw_track(broadcast, name, subscription, on_frame)
3118 })
3119}
3120
3121/// Update a raw track subscription's delivery preferences.
3122///
3123/// Pass NULL for `subscription` to reset to moq-net defaults.
3124///
3125/// Returns a zero on success, or a negative code on failure.
3126///
3127/// # Safety
3128/// - The caller must ensure that subscription is either NULL or a valid pointer to a [moq_subscription] struct.
3129#[unsafe(no_mangle)]
3130pub unsafe extern "C" fn moq_consume_track_update(track: u32, subscription: *const moq_subscription) -> i32 {
3131 ffi::enter(move || {
3132 let track = ffi::parse_id(track)?;
3133 let subscription = unsafe { subscription.as_ref() }.map(moq_net::track::Subscription::from);
3134 State::lock().consume.raw_track_update(track, subscription)
3135 })
3136}
3137
3138/// Read a raw frame's payload delivered via the [moq_consume_track] callback.
3139///
3140/// Fills `dst.payload` / `dst.payload_size`; the pointer is valid until the
3141/// frame is released with [moq_consume_frame_free]. `dst.timestamp_us` is the
3142/// frame presentation timestamp in microseconds. `dst.keyframe` is reported as
3143/// false because raw tracks do not parse codec metadata.
3144///
3145/// Returns a zero on success, or a negative code on failure.
3146///
3147/// # Safety
3148/// - The caller must ensure that `dst` is a valid pointer to a [moq_frame] struct.
3149#[unsafe(no_mangle)]
3150pub unsafe extern "C" fn moq_consume_track_frame(frame: u32, dst: *mut moq_frame) -> i32 {
3151 ffi::enter(move || {
3152 let frame = ffi::parse_id(frame)?;
3153 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
3154 State::lock().consume.raw_frame(frame, dst)
3155 })
3156}
3157
3158/// Free a raw frame delivered via the [moq_consume_track] callback, releasing its payload.
3159///
3160/// Returns a zero on success, or a negative code on failure.
3161#[unsafe(no_mangle)]
3162pub extern "C" fn moq_consume_track_frame_free(frame: u32) -> i32 {
3163 ffi::enter(move || {
3164 let frame = ffi::parse_id(frame)?;
3165 State::lock().consume.raw_frame_close(frame)
3166 })
3167}
3168
3169/// Stop a raw track consumer's background task.
3170///
3171/// Returns immediately: zero on success, or a negative code if already closed.
3172/// Does NOT free `user_data`; the [moq_consume_track] `on_frame` callback still
3173/// fires once more with a terminal `0` (or a negative error), which is where
3174/// `user_data` should be released. Frames already delivered via the callback
3175/// remain valid until released with [moq_consume_track_frame_free].
3176#[unsafe(no_mangle)]
3177pub extern "C" fn moq_consume_track_cancel(track: u32) -> i32 {
3178 ffi::enter(move || {
3179 let track = ffi::parse_id(track)?;
3180 State::lock().consume.raw_track_close(track)
3181 })
3182}
3183
3184/// Subscribe to a raw track's best-effort datagrams by name.
3185///
3186/// The datagram counterpart to [moq_consume_track], on its own subscription. `on_datagram`
3187/// is called with a positive datagram ID for each datagram in arrival order, then exactly
3188/// once more with a terminal code: `0` (closed cleanly) or a negative error. After the
3189/// terminal (`<= 0`) callback, `on_datagram` is never called again and `user_data` is never
3190/// touched again, so release `user_data` there. The terminal callback fires even after
3191/// [moq_consume_datagrams_cancel]. Read each datagram with [moq_consume_datagram] and release
3192/// it with [moq_consume_datagram_free]. Datagrams arrive only over datagram-capable
3193/// transports and lite-05 or newer moq-lite; there is no stream fallback.
3194///
3195/// Returns a non-zero handle to the subscription on success, or a negative code on failure.
3196///
3197/// # Safety
3198/// - The caller must ensure that name is a valid pointer to name_len bytes of data.
3199/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_datagram` callback.
3200#[unsafe(no_mangle)]
3201pub unsafe extern "C" fn moq_consume_datagrams(
3202 broadcast: u32,
3203 name: *const c_char,
3204 name_len: usize,
3205 on_datagram: ffi::moq_status_callback,
3206 user_data: *mut c_void,
3207) -> i32 {
3208 ffi::enter(move || {
3209 let broadcast = ffi::parse_id(broadcast)?;
3210 let name = unsafe { ffi::parse_str(name, name_len)? };
3211 let on_datagram = unsafe { ffi::OnStatus::new(user_data, on_datagram)? };
3212 State::lock().consume.datagram_track(broadcast, name, on_datagram)
3213 })
3214}
3215
3216/// Read a datagram delivered via the [moq_consume_datagrams] callback.
3217///
3218/// Fills `dst.payload` / `dst.payload_size` (valid until the datagram is released with
3219/// [moq_consume_datagram_free]), plus `dst.timestamp_us` and `dst.sequence`.
3220///
3221/// Returns a zero on success, or a negative code on failure.
3222///
3223/// # Safety
3224/// - The caller must ensure that `dst` is a valid pointer to a [moq_datagram] struct.
3225#[unsafe(no_mangle)]
3226pub unsafe extern "C" fn moq_consume_datagram(datagram: u32, dst: *mut moq_datagram) -> i32 {
3227 ffi::enter(move || {
3228 let datagram = ffi::parse_id(datagram)?;
3229 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
3230 State::lock().consume.datagram(datagram, dst)
3231 })
3232}
3233
3234/// Free a datagram delivered via the [moq_consume_datagrams] callback, releasing its payload.
3235///
3236/// Returns a zero on success, or a negative code on failure.
3237#[unsafe(no_mangle)]
3238pub extern "C" fn moq_consume_datagram_free(datagram: u32) -> i32 {
3239 ffi::enter(move || {
3240 let datagram = ffi::parse_id(datagram)?;
3241 State::lock().consume.datagram_close(datagram)
3242 })
3243}
3244
3245/// Stop a datagram subscription's background task.
3246///
3247/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
3248/// `user_data`; the [moq_consume_datagrams] `on_datagram` callback still fires once more with a
3249/// terminal `0` (or a negative error), which is where `user_data` should be released. Datagrams
3250/// already delivered via the callback remain valid until released with [moq_consume_datagram_free].
3251#[unsafe(no_mangle)]
3252pub extern "C" fn moq_consume_datagrams_cancel(task: u32) -> i32 {
3253 ffi::enter(move || {
3254 let task = ffi::parse_id(task)?;
3255 State::lock().consume.datagram_track_close(task)
3256 })
3257}
3258
3259/// Subscribe to a JSON snapshot track (lossy latest-value) by name.
3260///
3261/// `on_value` is called with a positive value ID for each new latest value; a consumer that
3262/// falls behind collapses the backlog and only sees the newest. It is called exactly once more
3263/// with a terminal `0` (track ended / closed) or a negative error, after which `user_data` is
3264/// never touched again, so release it there. Read each value with [moq_consume_json_value] and
3265/// release it with [moq_consume_json_value_free]. Pass the same compression the producer used.
3266///
3267/// Returns a non-zero handle to the task on success, or a negative code on failure.
3268///
3269/// # Safety
3270/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
3271/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_value` callback.
3272#[unsafe(no_mangle)]
3273pub unsafe extern "C" fn moq_consume_json_snapshot(
3274 broadcast: u32,
3275 name: *const c_char,
3276 name_len: usize,
3277 config: *const moq_json_snapshot_config,
3278 on_value: ffi::moq_status_callback,
3279 user_data: *mut c_void,
3280) -> i32 {
3281 ffi::enter(move || {
3282 let broadcast = ffi::parse_id(broadcast)?;
3283 let name = unsafe { ffi::parse_str(name, name_len)? };
3284 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
3285 let mut consumer = moq_json::snapshot::consumer::Config::default();
3286 consumer.compression = if config.compression {
3287 moq_json::Compression::Deflate
3288 } else {
3289 moq_json::Compression::None
3290 };
3291 let on_value = unsafe { ffi::OnStatus::new(user_data, on_value)? };
3292 State::lock().consume.json_snapshot(broadcast, name, consumer, on_value)
3293 })
3294}
3295
3296/// Subscribe to a JSON stream track (lossless append-log) by name.
3297///
3298/// `on_value` is called with a positive value ID for each record, in order, then once more with
3299/// a terminal `0` or negative error where `user_data` should be released. Read each value with
3300/// [moq_consume_json_value] and release it with [moq_consume_json_value_free].
3301///
3302/// Returns a non-zero handle to the task on success, or a negative code on failure.
3303///
3304/// # Safety
3305/// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer.
3306/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_value` callback.
3307#[unsafe(no_mangle)]
3308pub unsafe extern "C" fn moq_consume_json_stream(
3309 broadcast: u32,
3310 name: *const c_char,
3311 name_len: usize,
3312 config: *const moq_json_stream_config,
3313 on_value: ffi::moq_status_callback,
3314 user_data: *mut c_void,
3315) -> i32 {
3316 ffi::enter(move || {
3317 let broadcast = ffi::parse_id(broadcast)?;
3318 let name = unsafe { ffi::parse_str(name, name_len)? };
3319 let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?;
3320 let mut consumer = moq_json::stream::Config::default();
3321 if config.compression {
3322 consumer.compression = moq_json::Compression::Deflate;
3323 }
3324 let on_value = unsafe { ffi::OnStatus::new(user_data, on_value)? };
3325 State::lock().consume.json_stream(broadcast, name, consumer, on_value)
3326 })
3327}
3328
3329/// Read a JSON value delivered via a [moq_consume_json_snapshot] or [moq_consume_json_stream] callback.
3330///
3331/// Fills `dst.json` / `dst.json_len`; the pointer is valid until the value is released with
3332/// [moq_consume_json_value_free].
3333///
3334/// Returns a zero on success, or a negative code on failure.
3335///
3336/// # Safety
3337/// - The caller must ensure `dst` is a valid pointer to a [moq_json_value] struct.
3338#[unsafe(no_mangle)]
3339pub unsafe extern "C" fn moq_consume_json_value(value: u32, dst: *mut moq_json_value) -> i32 {
3340 ffi::enter(move || {
3341 let value = ffi::parse_id(value)?;
3342 let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
3343 State::lock().consume.json_value(value, dst)
3344 })
3345}
3346
3347/// Release a JSON value delivered via a consumer callback.
3348///
3349/// Returns a zero on success, or a negative code on failure.
3350#[unsafe(no_mangle)]
3351pub extern "C" fn moq_consume_json_value_free(value: u32) -> i32 {
3352 ffi::enter(move || {
3353 let value = ffi::parse_id(value)?;
3354 State::lock().consume.json_value_close(value)
3355 })
3356}
3357
3358/// Stop a JSON consumer's background task (snapshot or stream).
3359///
3360/// Returns immediately: zero on success, or a negative code if already closed. Does NOT free
3361/// `user_data`; the `on_value` callback still fires once more with a terminal `0` (or a negative
3362/// error), which is where `user_data` should be released. Values already delivered remain valid
3363/// until released with [moq_consume_json_value_free].
3364#[unsafe(no_mangle)]
3365pub extern "C" fn moq_consume_json_cancel(task: u32) -> i32 {
3366 ffi::enter(move || {
3367 let task = ffi::parse_id(task)?;
3368 State::lock().consume.json_close(task)
3369 })
3370}