use crate::ffi::ReturnCode; use crate::{Connect, Error, State, ffi, moq_protocol_error}; use std::ffi::c_char; use std::ffi::c_void; use std::str::FromStr; use tracing::Level; /// How a media track's frames are wrapped, independent of the codec. /// /// The ABI carries this as a `uint32_t`, so an unknown discriminant from C is an /// error rather than UB. #[repr(C)] #[expect(non_camel_case_types, reason = "C ABI type names match moq.h")] #[derive(Clone, Copy, Debug)] pub enum moq_container_kind { /// A QUIC varint timestamp prefix followed by the raw codec payload. /// Timestamps are in microseconds. MOQ_CONTAINER_KIND_LEGACY = 0, /// Fragmented MP4: each frame is a complete moof+mdat fragment, described by /// the init segment in `moq_container::init`. MOQ_CONTAINER_KIND_CMAF = 1, /// Low Overhead Container (draft-ietf-moq-loc): a small property block /// followed by the codec payload. MOQ_CONTAINER_KIND_LOC = 2, /// A container this build does not recognize, so the rendition must be /// ignored. Only ever read out of a catalog: publishing it is an error. MOQ_CONTAINER_KIND_UNKNOWN = 3, } /// The container of a video or audio rendition, plus whatever that container /// needs to describe itself. /// /// Zeroing this struct means `MOQ_CONTAINER_KIND_LEGACY` with no init segment, /// which is what a rendition written by [moq_publish_audio] or [moq_publish_video] carries. #[repr(C)] #[derive(Clone, Copy)] pub struct moq_container { /// `moq_container_kind` discriminant. pub kind: u32, /// The CMAF init segment (ftyp+moov), or NULL. /// Read only when `kind` is `MOQ_CONTAINER_KIND_CMAF`, where it is required. pub init: *const u8, /// The length in bytes of `init`. pub init_len: usize, } impl Default for moq_container { fn default() -> Self { Self { kind: moq_container_kind::MOQ_CONTAINER_KIND_LEGACY as u32, init: std::ptr::null(), init_len: 0, } } } /// # Safety /// - `container->init` must point to `container->init_len` bytes when /// `container->kind` is `MOQ_CONTAINER_KIND_CMAF`. pub(crate) unsafe fn parse_container(container: &moq_container) -> Result { use hang::catalog::Container; Ok(match container.kind { v if v == moq_container_kind::MOQ_CONTAINER_KIND_LEGACY as u32 => Container::Legacy, v if v == moq_container_kind::MOQ_CONTAINER_KIND_CMAF as u32 => { let init = unsafe { ffi::parse_slice(container.init, container.init_len)? }; // A CMAF rendition is undecodable without its init segment, so an empty one // fails here rather than at every subscriber. if init.is_empty() { return Err(Error::InvalidPointer); } Container::Cmaf { init: bytes::Bytes::copy_from_slice(init), } } v if v == moq_container_kind::MOQ_CONTAINER_KIND_LOC as u32 => Container::Loc, // UNKNOWN included: we kept none of the original JSON, so there is nothing to republish. _ => return Err(Error::InvalidCode), }) } /// Describe a catalog container for C, borrowing the CMAF init segment rather /// than copying it, so the result lives only as long as the catalog snapshot. pub(crate) fn borrow_container(container: &hang::catalog::Container) -> moq_container { use hang::catalog::Container; let (kind, init) = match container { Container::Legacy => (moq_container_kind::MOQ_CONTAINER_KIND_LEGACY, None), Container::Cmaf { init } => (moq_container_kind::MOQ_CONTAINER_KIND_CMAF, Some(init)), Container::Loc => (moq_container_kind::MOQ_CONTAINER_KIND_LOC, None), Container::Unknown(_) => (moq_container_kind::MOQ_CONTAINER_KIND_UNKNOWN, None), }; moq_container { kind: kind as u32, init: init.map_or(std::ptr::null(), |init| init.as_ptr()), init_len: init.map_or(0, |init| init.len()), } } /// A single audio codec [moq_publish_audio] can parse. #[repr(C)] #[expect(non_camel_case_types, reason = "C ABI type names match moq.h")] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum moq_audio_format { /// Advanced Audio Coding, configured by an AudioSpecificConfig. MOQ_AUDIO_FORMAT_AAC = 0, /// Opus, configured by an OpusHead. MOQ_AUDIO_FORMAT_OPUS = 1, /// FLAC, configured by the `fLaC` marker plus its STREAMINFO block. MOQ_AUDIO_FORMAT_FLAC = 2, /// MPEG-1/2 Audio Layer III. MOQ_AUDIO_FORMAT_MP3 = 3, } /// A single video codec [moq_publish_video] can parse. /// /// H.264 and H.265 appear twice each because the framing differs, not just the /// codec: AVC1/HVC1 are length-prefixed with an out-of-band config record, /// while AVC3/HEV1 are Annex-B with the parameter sets inline. #[repr(C)] #[expect(non_camel_case_types, reason = "C ABI type names match moq.h")] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum moq_video_format { /// H.264, length-prefixed NALUs with an out-of-band avcC. MOQ_VIDEO_FORMAT_AVC1 = 0, /// H.264, Annex-B with inline SPS/PPS. MOQ_VIDEO_FORMAT_AVC3 = 1, /// H.265, length-prefixed NALUs with an out-of-band hvcC. MOQ_VIDEO_FORMAT_HVC1 = 2, /// H.265, Annex-B with inline parameter sets. MOQ_VIDEO_FORMAT_HEV1 = 3, /// AV1. MOQ_VIDEO_FORMAT_AV01 = 4, /// VP8. MOQ_VIDEO_FORMAT_VP8 = 5, /// VP9. MOQ_VIDEO_FORMAT_VP9 = 6, } /// A container [moq_publish_container] can demux, which may publish several tracks. #[repr(C)] #[expect(non_camel_case_types, reason = "C ABI type names match moq.h")] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum moq_container_format { /// Fragmented MP4 / CMAF, split into one frame per sample. MOQ_CONTAINER_FORMAT_FMP4 = 0, /// Matroska / WebM. MOQ_CONTAINER_FORMAT_MKV = 1, /// MPEG-2 transport stream. MOQ_CONTAINER_FORMAT_TS = 2, /// Flash Video, as used by RTMP. MOQ_CONTAINER_FORMAT_FLV = 3, } /// Configuration for [moq_publish_audio]. /// /// Zero the struct, then set `format` and the required `init` bytes. New /// optional fields are appended so existing initializers keep their meaning. #[repr(C)] pub struct moq_audio_init { /// The audio codec, a [moq_audio_format] value. pub format: u32, /// Codec init bytes: an OpusHead, an AudioSpecificConfig, a STREAMINFO. /// Required, since audio has no in-band config to resolve from frames. pub init: *const u8, /// Length of `init` in bytes. pub init_len: usize, /// Human-readable rendition name for track pickers, or NULL if not used. pub label: *const c_char, /// Length of `label` in bytes. pub label_len: usize, /// Catalog fields the init bytes cannot reveal. Zeroed means none. pub hint: moq_audio_hint, } /// Optional catalog fields for [moq_audio_init::hint]. /// /// Zero the struct and set only the `has_*` flags you want. A supplied field is /// published as given and is never re-estimated. An absent one is measured from /// the frames, as without a hint. A flag set with a 0 value is an invalid config. /// /// New optional fields are appended so existing initializers keep their meaning. #[repr(C)] #[derive(Clone, Copy, Default)] pub struct moq_audio_hint { /// Maximum bitrate in bits per second when `has_bitrate` is true. pub bitrate: u64, /// Whether `bitrate` is present. pub has_bitrate: bool, /// The longest span between two group starts on this track, in /// microseconds, when `has_group_duration` is true: a video GOP, or the span /// between the audio cuts you make. An upper bound, so a broadcast may take /// the largest across its tracks. Must be at least 1 and at most /// 2^53 - 1 milliseconds. pub group_duration_us: u64, /// Whether `group_duration_us` is present. pub has_group_duration: bool, } impl moq_audio_hint { /// Copy the supplied fields onto the importer's init. fn apply(&self, init: &mut moq_mux::import::AudioInit) -> Result<(), Error> { init.bitrate = supplied_bitrate(self.has_bitrate, self.bitrate)?; Ok(()) } /// The supplied group duration, for the track's HLS records. fn group_duration(&self) -> Result, Error> { group_duration(self.has_group_duration, self.group_duration_us) } } /// The longest group duration a hint may give: 2^53 - 1 milliseconds, the largest /// integer a JavaScript reader of the catalog holds exactly. const MAX_GROUP_DURATION_US: u64 = ((1 << 53) - 1) * 1000; /// A supplied group duration, `None` when absent, or an error for 0 or past /// [MAX_GROUP_DURATION_US]. fn group_duration(present: bool, us: u64) -> Result, Error> { if !present { return Ok(None); } if us == 0 || us > MAX_GROUP_DURATION_US { return Err(Error::InvalidConfig(format!( "group_duration_us must be 1..={MAX_GROUP_DURATION_US}, got {us}" ))); } Ok(Some(std::time::Duration::from_micros(us))) } /// A supplied bitrate, `None` when absent, or an error for 0. fn supplied_bitrate(present: bool, bitrate: u64) -> Result, Error> { match (present, bitrate) { (false, _) => Ok(None), (true, 0) => Err(Error::InvalidConfig( "bitrate must not be 0 when has_bitrate is set".into(), )), (true, bitrate) => Ok(Some(bitrate)), } } /// Configuration for [moq_publish_video]. /// /// Zero the struct, then set `format` and whatever else the codec needs. `init` /// may stay NULL for a format that resolves in band. #[repr(C)] pub struct moq_video_init { /// The video codec, a [moq_video_format] value. pub format: u32, /// Codec init bytes (an avcC, an hvcC), or NULL for a format that resolves /// from the stream itself. pub init: *const u8, /// Length of `init` in bytes. pub init_len: usize, /// Human-readable rendition name for track pickers, or NULL if not used. pub label: *const c_char, /// Length of `label` in bytes. pub label_len: usize, /// Catalog fields the bitstream cannot reveal itself. Zeroed means none. pub hint: moq_video_hint, } /// Optional catalog fields for [moq_video_init::hint]. /// /// Zero the struct and set only the `has_*` flags you want. Hints fill gaps the /// bitstream leaves (especially bitrate); a value the stream detects later wins /// for dimensions. #[repr(C)] #[derive(Clone, Copy, Default)] pub struct moq_video_hint { /// Encoded width in pixels when `has_coded` is true. pub coded_width: u32, /// Encoded height in pixels when `has_coded` is true. pub coded_height: u32, /// Whether `coded_width` and `coded_height` are present. pub has_coded: bool, /// Maximum bitrate in bits per second when `has_bitrate` is true; it must not be 0. pub bitrate: u64, /// Whether `bitrate` is present. pub has_bitrate: bool, /// Frame rate when `has_framerate` is true. pub framerate: f64, /// Whether `framerate` is present. pub has_framerate: bool, /// Latency-optimized decode when `has_optimize_for_latency` is true. pub optimize_for_latency: bool, /// Whether `optimize_for_latency` is present. pub has_optimize_for_latency: bool, /// The longest span between two group starts on this track, in /// microseconds, when `has_group_duration` is true: a video GOP, or the span /// between the audio cuts you make. An upper bound, so a broadcast may take /// the largest across its tracks. Must be at least 1 and at most /// 2^53 - 1 milliseconds. pub group_duration_us: u64, /// Whether `group_duration_us` is present. pub has_group_duration: bool, } impl moq_video_hint { /// The catalog hint these flags describe, or an error for a bitrate flag set with 0. fn resolve(&self) -> Result { let mut out = moq_mux::catalog::VideoHint::default(); if self.has_coded { out.coded_width = Some(self.coded_width); out.coded_height = Some(self.coded_height); } out.bitrate = supplied_bitrate(self.has_bitrate, self.bitrate)?; if self.has_framerate { out.framerate = Some(self.framerate); } if self.has_optimize_for_latency { out.optimize_for_latency = Some(self.optimize_for_latency); } Ok(out) } /// The supplied group duration, for the track's HLS records. fn group_duration(&self) -> Result, Error> { group_duration(self.has_group_duration, self.group_duration_us) } } /// Configuration for [moq_publish_container]. /// /// There is no label here: a container publishes and describes its own tracks, /// so a rendition name would have no single track to land on. #[repr(C)] pub struct moq_container_init { /// The container format, a [moq_container_format] value. pub format: u32, /// The leading chunk of the container, decoded immediately, or NULL. pub init: *const u8, /// Length of `init` in bytes. pub init_len: usize, } /// Validate an audio format code from C. /// /// The field is a `u32` rather than the enum: C can put any integer there, and matching an /// out-of-range discriminant as a Rust enum is UB. Same reason as [moq_audio_sample_format]. fn audio_format_from_u32(value: u32) -> Result { use moq_mux::import::AudioFormat; Ok(match value { v if v == moq_audio_format::MOQ_AUDIO_FORMAT_AAC as u32 => AudioFormat::Aac, v if v == moq_audio_format::MOQ_AUDIO_FORMAT_OPUS as u32 => AudioFormat::Opus, v if v == moq_audio_format::MOQ_AUDIO_FORMAT_FLAC as u32 => AudioFormat::Flac, v if v == moq_audio_format::MOQ_AUDIO_FORMAT_MP3 as u32 => AudioFormat::Mp3, _ => return Err(Error::InvalidCode), }) } /// Validate a video format code from C. See [audio_format_from_u32]. fn video_format_from_u32(value: u32) -> Result { use moq_mux::import::VideoFormat; Ok(match value { v if v == moq_video_format::MOQ_VIDEO_FORMAT_AVC1 as u32 => VideoFormat::Avc1, v if v == moq_video_format::MOQ_VIDEO_FORMAT_AVC3 as u32 => VideoFormat::Avc3, v if v == moq_video_format::MOQ_VIDEO_FORMAT_HVC1 as u32 => VideoFormat::Hvc1, v if v == moq_video_format::MOQ_VIDEO_FORMAT_HEV1 as u32 => VideoFormat::Hev1, v if v == moq_video_format::MOQ_VIDEO_FORMAT_AV01 as u32 => VideoFormat::Av01, v if v == moq_video_format::MOQ_VIDEO_FORMAT_VP8 as u32 => VideoFormat::Vp8, v if v == moq_video_format::MOQ_VIDEO_FORMAT_VP9 as u32 => VideoFormat::Vp9, _ => return Err(Error::InvalidCode), }) } /// Validate a container format code from C. See [audio_format_from_u32]. fn container_format_from_u32(value: u32) -> Result { use moq_mux::import::ContainerFormat; Ok(match value { v if v == moq_container_format::MOQ_CONTAINER_FORMAT_FMP4 as u32 => ContainerFormat::Fmp4, v if v == moq_container_format::MOQ_CONTAINER_FORMAT_MKV as u32 => ContainerFormat::Mkv, v if v == moq_container_format::MOQ_CONTAINER_FORMAT_TS as u32 => ContainerFormat::Ts, v if v == moq_container_format::MOQ_CONTAINER_FORMAT_FLV as u32 => ContainerFormat::Flv, _ => return Err(Error::InvalidCode), }) } /// Information about a video rendition in the catalog. #[repr(C)] pub struct moq_video_config { /// The name of the track, NOT NULL terminated. pub name: *const c_char, /// The length in bytes of `name`. pub name_len: usize, /// The codec of the track, NOT NULL terminated pub codec: *const c_char, /// The length in bytes of `codec`. pub codec_len: usize, /// The description of the track, or NULL if not used. /// This is codec specific, for example H264: /// - NULL: annex.b encoded /// - Non-NULL: AVCC encoded pub description: *const u8, /// The length in bytes of `description`. pub description_len: usize, /// The encoded width/height of the media, a hint so a decoder can size its /// buffers up front. Zero means absent, which no valid dimension is, so the /// two are independent: a catalog carrying only one round-trips unchanged. pub coded_width: u32, /// The encoded height in pixels, or zero when absent. pub coded_height: u32, /// How the track's frames are wrapped. pub container: moq_container, /// Human-readable rendition name for track pickers, or NULL if not used. pub label: *const c_char, /// Length of `label` in bytes. pub label_len: usize, /// Higher values are preferred; compatibility transcodes use -1 and the default is 0. pub preference: i32, } /// Catalog properties shared by every video rendition. /// /// A false `has_*` flag clears that field from the next catalog rather than preserving its previous value. #[repr(C)] #[derive(Clone, Copy, Default)] pub struct moq_video_properties { /// Final rendered width in pixels when `has_display` is true. pub display_width: u32, /// Final rendered height in pixels when `has_display` is true. pub display_height: u32, /// Whether `display_width` and `display_height` are present. pub has_display: bool, /// Clockwise rotation in degrees when `has_rotation` is true. pub rotation: f64, /// Whether `rotation` is present. pub has_rotation: bool, /// Whether to flip horizontally after rotation when `has_flip` is true. pub flip: bool, /// Whether `flip` is present. pub has_flip: bool, } /// Information about an audio rendition in the catalog. #[repr(C)] pub struct moq_audio_config { /// The name of the track, NOT NULL terminated pub name: *const c_char, /// The length in bytes of `name`. pub name_len: usize, /// The codec of the track, NOT NULL terminated pub codec: *const c_char, /// The length in bytes of `codec`. pub codec_len: usize, /// The description of the track, or NULL if not used. pub description: *const u8, /// The length in bytes of `description`. pub description_len: usize, /// The sample rate of the track in Hz pub sample_rate: u32, /// The number of channels in the track pub channel_count: u32, /// How the track's frames are wrapped. pub container: moq_container, /// Human-readable rendition name for track pickers, or NULL if not used. pub label: *const c_char, /// Length of `label` in bytes. pub label_len: usize, } /// Options for a JSON snapshot track (lossy latest-value mode). /// /// The same config is passed to a producer and its consumers, but the consumer reads only /// `compression`; `deltas` is producer-only. #[repr(C)] pub struct moq_json_snapshot_config { /// Encode updates as deltas against the group's snapshot. When off, each update opens a new /// group with a full snapshot. When on, a join after more than eight snapshots' worth of /// deltas asks for the value restated in a fresh group. Ignored by the consumer. pub deltas: bool, /// DEFLATE-compress each group. Must match on the producer and consumer. pub compression: bool, /// The track's maximum bitrate in bits per second when `has_bitrate` is true. /// Supplied, it is published as given and is never re-estimated; it must not /// be 0. Ignored by the consumer. pub bitrate: u64, /// Whether `bitrate` is present. pub has_bitrate: bool, } /// Options for a JSON stream track (lossless append-log mode). #[repr(C)] pub struct moq_json_stream_config { /// DEFLATE-compress the group. Must match on the producer and consumer. pub compression: bool, /// The track's maximum bitrate in bits per second when `has_bitrate` is true. /// Supplied, it is published as given and is never re-estimated; it must not /// be 0. Ignored by the consumer. pub bitrate: u64, /// Whether `bitrate` is present. pub has_bitrate: bool, } /// Options for a binary data track, in either mode. /// /// The mode is fixed by which constructor is called ([moq_publish_binary_snapshot] or /// [moq_publish_binary_stream]), so it is not in here. #[repr(C)] pub struct moq_binary_config { /// DEFLATE-compress each payload, advertised in the catalog entry. pub compression: bool, /// The payloads' media type (e.g. `image/jpeg`), or NULL to leave it unstated. pub mime: *const c_char, /// Length of `mime` in bytes. pub mime_len: usize, /// The track's maximum bitrate in bits per second when `has_bitrate` is true. /// Supplied, it is published as given and is never re-estimated; it must not /// be 0. Ignored by the consumer. pub bitrate: u64, /// Whether `bitrate` is present. pub has_bitrate: bool, } /// A JSON value delivered by a consumer callback. #[repr(C)] pub struct moq_json_value { /// The JSON document as UTF-8, NOT NULL terminated. pub json: *const c_char, /// The length in bytes of `json`. pub json_len: usize, /// The timestamp of the frame that carried the value in microseconds, or 0 when /// `timestamp_present` is false. Rounded down to a whole microsecond, so a tick on a track /// like 90 kHz may not survive a read and republish exactly. pub timestamp_us: u64, /// Whether the frame carries a timestamp. A value read from an untimed track does not. pub timestamp_present: bool, } /// Information about a frame of media. #[repr(C)] pub struct moq_frame { /// The payload of the frame, or NULL/0 if the stream has ended pub payload: *const u8, /// The length in bytes of `payload`. pub payload_size: usize, /// The presentation timestamp of the frame in microseconds, or 0 when /// `timestamp_present` is false. pub timestamp_us: u64, /// Whether the frame carries a timestamp. Media frames always do; a raw frame read /// from an untimed track does not. pub timestamp_present: bool, /// Whether this frame opens a group or is a video keyframe; audio is true only at a group start. pub keyframe: bool, } /// One encoded media read; `kind` selects the filled field and the others are zeroed. #[repr(C)] pub struct moq_media_read { /// Frame, gap, or encoder end, as for decoded media. pub kind: crate::moq_decode_read_kind, /// Encoded payload and metadata for a frame read. pub frame: moq_frame, /// Lost media for a gap read; what follows does not continue it. pub gap: crate::moq_decode_gap, /// Where a stopped encoder's media stops, in microseconds. Following frames before /// this point are its remaining media; the first at or past it comes from a fresh encoder. pub end_us: u64, } /// A best-effort raw track datagram delivered via [moq_consume_datagrams]. #[repr(C)] pub struct moq_datagram { /// The payload of the datagram, or NULL/0 if the track has ended. pub payload: *const u8, /// The length in bytes of `payload`. pub payload_size: usize, /// The presentation timestamp of the datagram in microseconds, or 0 when /// `timestamp_present` is false. pub timestamp_us: u64, /// Whether the datagram carries a timestamp. One read from an untimed track does not. pub timestamp_present: bool, /// Per-track sequence number, drawn from the same namespace as groups. pub sequence: u64, } /// Publisher-side raw track properties. /// /// A null [moq_publish_track] `info` pointer uses the moq-net defaults, and so does a /// zero-initialized struct. #[repr(C)] pub struct moq_track_info { /// Priority, used to break ties between subscriptions of equal subscriber priority. /// Higher is more urgent; the default 0 is the midpoint. pub priority: i8, /// Maximum age of a non-latest group before the publisher evicts it, in microseconds. /// The publisher-side half of `moq_subscription.max_delay_us`. pub max_age_us: u64, /// Whether `max_age_us` is set. When false, the publisher imposes no age limit. pub max_age_present: bool, /// Per-frame timescale in ticks per second. pub timescale: u64, /// Whether `timescale` is set. When false, the default microsecond timescale /// applies, matching the `timestamp_us` units used everywhere else in this ABI. pub timescale_present: bool, /// A joining subscriber asks for a new group once the newest group's bytes after its /// first frame exceed `refresh` times that frame. `0` asks once any frame follows the first. pub refresh: u32, /// Whether `refresh` is set. When false, a joining subscriber never asks for a new group. pub refresh_present: bool, /// Whether each group is DEFLATE-compressed on the wire to peers that support it. /// Readers always see the original frames. pub compress: bool, } impl TryFrom<&moq_track_info> for moq_net::track::Info { type Error = Error; fn try_from(info: &moq_track_info) -> Result { // Raw tracks default to a microsecond timescale, matching the C ABI's // timestamp_us units. An explicit timescale below overrides it. let mut out = moq_net::track::Info::default() .with_timescale(moq_net::Timescale::MICRO) .with_priority(info.priority); if info.max_age_present { out = out.with_max_age(std::time::Duration::from_micros(info.max_age_us)); } if info.timescale_present { out = out.with_timescale(moq_net::Timescale::new(info.timescale)?); } if info.refresh_present { out = out.with_refresh(info.refresh); } Ok(out.with_compress(info.compress)) } } /// Whether a published track has subscribers, as reported by a demand watcher. /// /// The positive values an `on_demand` callback receives; `0` and negative codes are /// the terminal statuses every callback shares. [moq_publish_demand_wanted] reads which /// group the subscribers want. #[repr(C)] #[expect(non_camel_case_types, reason = "C ABI type names match moq.h")] #[derive(Clone, Copy, Debug)] pub enum moq_demand { /// At least one subscriber is active. Repeats when the wanted group moves. MOQ_DEMAND_USED = 1, /// No subscriber is active. MOQ_DEMAND_UNUSED = 2, } /// Subscriber-side raw track delivery preferences. /// /// A null [moq_consume_track] or [moq_consume_track_update] `subscription` /// pointer uses the moq-net defaults, and so does a zeroed struct. #[repr(C)] pub struct moq_subscription { /// Delivery priority. Higher values preempt lower ones under contention; the default 0 is /// the midpoint. pub priority: i8, /// How far a non-latest group may fall behind the live edge before it is skipped, in microseconds. /// Zero skips immediately. Enforced by the publisher's cache and by any local buffering. /// It caps every range. pub max_delay_us: u64, /// Whether `max_delay_us` is set. When false, moq-net's default applies: zero. pub max_delay_present: bool, /// The spans of the track to deliver, or NULL for everything `max_delay_us` still /// considers fresh. A non-NULL pointer with `ranges_len` 0 asks for nothing. /// /// A request to the publisher, aggregated across every subscriber, and this /// subscription's read filter: it reads only the groups its own ranges cover. pub ranges: *const moq_range, /// The number of entries in `ranges`. pub ranges_len: usize, /// `moq_order` discriminant: which pending group the publisher sends first. pub order: u32, } /// A span of a track to deliver: from `start` up to `end`, or onward without one. /// /// A relative or time start is the first frame of its group, and a relative or time end /// includes its whole group. An absolute end is exclusive. #[repr(C)] #[derive(Clone, Copy)] pub struct moq_range { /// Where delivery starts. pub start: moq_anchor, /// Where delivery stops, read only when `end_present` is true. pub end: moq_anchor, /// Whether `end` is present. When false, the range follows the live edge. pub end_present: bool, } /// Where one end of a [moq_range] falls. Zeroing it means group 0. #[repr(C)] #[derive(Clone, Copy)] pub struct moq_anchor { /// `moq_anchor_kind` discriminant. pub kind: u32, /// The group, or with `MOQ_ANCHOR_RELATIVE` how many groups before the latest one /// (0 is the latest, and an offset past the first group clamps to it). pub group: u64, /// The presentation time in microseconds, read only with `MOQ_ANCHOR_TIME`. pub timestamp_us: u64, } /// How a [moq_anchor] names its position. #[repr(C)] #[expect(non_camel_case_types, reason = "C ABI type names match moq.h")] #[derive(Clone, Copy, Debug)] pub enum moq_anchor_kind { /// A fixed group, delivered from its first frame. MOQ_ANCHOR_ABSOLUTE = 0, /// A count of groups back from the latest one. MOQ_ANCHOR_RELATIVE = 1, /// The group containing a presentation time. An untimed track refuses it. MOQ_ANCHOR_TIME = 2, } /// Which of a subscription's pending groups the publisher sends first. Zero is the default. #[repr(C)] #[expect(non_camel_case_types, reason = "C ABI type names match moq.h")] #[derive(Clone, Copy, Debug)] pub enum moq_order { /// Newest first, so congestion sheds the backlog rather than the live edge. MOQ_ORDER_DESCENDING = 0, /// Oldest first, draining a backlog in sequence order. MOQ_ORDER_ASCENDING = 1, } fn parse_anchor(anchor: &moq_anchor) -> Result { Ok(match anchor.kind { v if v == moq_anchor_kind::MOQ_ANCHOR_ABSOLUTE as u32 => moq_net::track::Anchor::Absolute(anchor.group), v if v == moq_anchor_kind::MOQ_ANCHOR_RELATIVE as u32 => moq_net::track::Anchor::Relative(anchor.group), v if v == moq_anchor_kind::MOQ_ANCHOR_TIME as u32 => { moq_net::track::Anchor::Time(moq_net::Timestamp::from_micros(anchor.timestamp_us)?) } _ => return Err(Error::InvalidCode), }) } /// # Safety /// - `subscription->ranges` must be NULL or point to `subscription->ranges_len` ranges. pub(crate) unsafe fn parse_subscription( subscription: &moq_subscription, ) -> Result { let order = match subscription.order { v if v == moq_order::MOQ_ORDER_DESCENDING as u32 => moq_net::track::Order::Descending, v if v == moq_order::MOQ_ORDER_ASCENDING as u32 => moq_net::track::Order::Ascending, _ => return Err(Error::InvalidCode), }; let mut out = moq_net::track::Subscription::default() .with_priority(subscription.priority) .with_order(order); if subscription.max_delay_present { out = out.with_max_delay(std::time::Duration::from_micros(subscription.max_delay_us)); } if !subscription.ranges.is_null() { let ranges = unsafe { std::slice::from_raw_parts(subscription.ranges, subscription.ranges_len) }; let ranges = ranges .iter() .map(|range| { Ok(moq_net::track::Range { start: parse_anchor(&range.start)?, end: range.end_present.then(|| parse_anchor(&range.end)).transpose()?, }) }) .collect::, Error>>()?; out = out.with_ranges(ranges); } Ok(out) } /// A borrowed UTF-8 string slice, NOT NULL terminated. /// /// Used in both directions. As an output (e.g. a JSON document moq-c hands back) the /// pointer borrows moq-c's own storage and is only valid until the owning resource is /// freed; see the function that fills it for the exact lifetime. As an input (e.g. a /// [moq_client_config] list) the pointer borrows the caller's storage and is only read /// during the call. #[repr(C)] #[derive(Clone, Copy)] pub struct moq_string { /// Pointer to `len` bytes of UTF-8, NOT NULL terminated. pub data: *const c_char, /// The string length in bytes. pub len: usize, } /// One untyped application catalog section: a name and its JSON value. /// /// Both `name` and `json` are UTF-8, NOT NULL terminated, and borrow the catalog /// snapshot's storage. They stay valid until the snapshot is freed with /// [moq_consume_catalog_free]. `json` is the section's value serialized as JSON /// (parse it yourself); a top-level catalog key beyond `video`/`audio`. #[repr(C)] pub struct moq_section { /// The section name, NOT NULL terminated. pub name: *const c_char, /// The length in bytes of `name`. pub name_len: usize, /// The section value as a JSON document, NOT NULL terminated. pub json: *const c_char, /// The length in bytes of `json`. pub json_len: usize, } /// A route advertisement: hops and a static cost. /// /// Pair with [moq_publish_announce] or [moq_origin_requests]. Zeroed (NULL hops, /// hops_len 0, cost 0) is the default route. `hops` is borrowed for the duration /// of the call that reads it. /// /// `cost` is the static production and link price; lower wins. #[repr(C)] #[derive(Clone, Copy)] pub struct moq_route { /// Hop ids, oldest first. NULL when `hops_len` is 0. 0 is the anonymous /// mark and is legal on a received chain. pub hops: *const u64, /// The number of entries in `hops`. pub hops_len: usize, /// Preference among routes covering the same prefix: lower wins. pub cost: u64, } impl Default for moq_route { fn default() -> Self { Self { hops: std::ptr::null(), hops_len: 0, cost: 0, } } } /// Parse a [moq_route], treating NULL as the default. /// /// # Safety /// `route` may be NULL, or must point at a readable [moq_route] whose `hops` /// pointer is valid for `hops_len` elements. unsafe fn parse_route(route: *const moq_route) -> Result { let Some(route) = (unsafe { route.as_ref() }) else { return Ok(moq_net::origin::Route::default()); }; let mut route_hops = moq_net::Hops::new(); if route.hops_len > 0 { if route.hops.is_null() { return Err(Error::InvalidPointer); } let hops = unsafe { std::slice::from_raw_parts(route.hops, route.hops_len) }; for id in hops { let hop = if *id == 0 { moq_net::Hop::UNKNOWN } else { moq_net::Hop::new(*id).map_err(|e| Error::InvalidConfig(e.to_string()))? }; route_hops.push(hop).map_err(|e| Error::InvalidConfig(e.to_string()))?; } } Ok(moq_net::origin::Route::default() .with_cost(moq_net::origin::Cost::new(route.cost)) .with_hops(route_hops)) } /// Which broadcasts [moq_origin_announced] lists. /// /// NULL or zeroed lists every visible path. Strings are borrowed for the /// duration of the call. New fields always append, so a zeroed struct keeps /// meaning the defaults. #[repr(C)] pub struct moq_announce_config { /// A literal path root, NOT NULL terminated. NULL when `prefix_len` is 0. pub prefix: *const c_char, /// The length in bytes of `prefix`. pub prefix_len: usize, /// A pattern relative to `prefix`, NOT NULL terminated, or NULL for every /// path beneath it. Empty is a valid exact filter. pub filter: *const c_char, /// The length in bytes of `filter`. pub filter_len: usize, /// Also list hidden paths: those with a segment starting with `.` below the prefix. pub hidden: bool, } /// Which announce event a [moq_announce_event] reports. #[repr(C)] #[expect(non_camel_case_types, reason = "C ABI type names match moq.h")] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum moq_announce_kind { /// A route now covers the prefix; the listener had none there. MOQ_ANNOUNCE_KIND_START = 0, /// The route covering the prefix changed hops or cost. MOQ_ANNOUNCE_KIND_UPDATE = 1, /// No route covers the prefix any more. MOQ_ANNOUNCE_KIND_END = 2, /// Another publisher instance now serves the prefix: drop what was resolved under /// it and request afresh. MOQ_ANNOUNCE_KIND_RESTART = 3, } /// An announce event from an origin: a route starting, updating, ending, or restarting. #[repr(C)] pub struct moq_announce_event { /// The covered prefix, relative to the origin, NOT NULL terminated pub prefix: *const c_char, /// The length in bytes of `prefix`. pub prefix_len: usize, /// What each requested filter wildcard matched. Each string is NOT NULL terminated. /// Meaningful only when `has_captures` is true; false means the route overlaps /// the filter without pinning every wildcard. pub captures: *const moq_string, /// The number of entries in `captures`. pub captures_len: usize, /// Whether `captures` is present. pub has_captures: bool, /// Which event this is. pub kind: moq_announce_kind, } /// Where a session is in its lifecycle, reported by [moq_connection_state]. #[repr(C)] #[expect(non_camel_case_types, reason = "C ABI type names match moq.h")] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum moq_connection_phase { /// No connection has been established yet. MOQ_CONNECTION_PHASE_CONNECTING = 0, /// A connection is serving. MOQ_CONNECTION_PHASE_CONNECTED = 1, /// No connection is serving while the session redials. MOQ_CONNECTION_PHASE_DISCONNECTED = 2, /// The old connection serves while its replacement dials. MOQ_CONNECTION_PHASE_MIGRATING = 3, /// The session has stopped for good: the terminal snapshot. MOQ_CONNECTION_PHASE_CLOSED = 4, } /// One coherent snapshot of a session's lifecycle, delivered to its `moq_state_callback`. /// /// The struct and its `protocol` are valid only during the callback, so copy what you keep. #[repr(C)] pub struct moq_connection_state { /// The lifecycle phase. pub phase: moq_connection_phase, /// How many connections have been established, including reconnects. pub connects: u64, /// Zero, or the negative code of the latest failed dial, dropped connection, or terminal /// error, which [moq_error] and [moq_error_protocol] describe during the callback. A /// connect clears it; a terminal zero is a clean close. pub failure: i32, /// The live connection's negotiated draft name, or empty while none is serving. pub protocol: moq_string, } /// Statistics and protocol sampled from the same connection by [moq_session_snapshot]. #[repr(C)] pub struct moq_connection_snapshot { /// Transport statistics, with per-metric availability flags. pub stats: moq_connection_stats, /// Negotiated draft name, backed by static storage valid for the process lifetime. pub protocol: moq_string, } /// A snapshot of connection statistics, filled in by [moq_session_stats]. /// /// Each metric has a `*_valid` flag: when `false`, the matching value is meaningless because /// the transport backend doesn't report it (a `false` flag is NOT the same as a zero value). /// Native QUIC reports every metric; the browser WebTransport reports few or none. Initialize /// the struct to zero before the call; [moq_session_stats] overwrites every field. #[repr(C)] pub struct moq_connection_stats { /// Smoothed round-trip time, in microseconds. pub rtt_us: u64, /// Whether `rtt_us` is available in this sample. pub rtt_valid: bool, /// Estimated send bandwidth from the congestion controller, in bits per second. pub estimated_send_rate_bps: u64, /// Whether `estimated_send_rate_bps` is available in this sample. pub estimated_send_rate_valid: bool, /// Total bytes sent, including retransmissions and overhead. pub bytes_sent: u64, /// Whether `bytes_sent` is available in this sample. pub bytes_sent_valid: bool, /// Total bytes received, including duplicates and overhead. pub bytes_received: u64, /// Whether `bytes_received` is available in this sample. pub bytes_received_valid: bool, /// Total bytes lost (detected via retransmission or acknowledgement). pub bytes_lost: u64, /// Whether `bytes_lost` is available in this sample. pub bytes_lost_valid: bool, /// Total datagrams sent. pub packets_sent: u64, /// Whether `packets_sent` is available in this sample. pub packets_sent_valid: bool, /// Total datagrams received. pub packets_received: u64, /// Whether `packets_received` is available in this sample. pub packets_received_valid: bool, /// Total datagrams detected as lost. pub packets_lost: u64, /// Whether `packets_lost` is available in this sample. pub packets_lost_valid: bool, } impl From<&moq_net::session::Stats> for moq_connection_stats { fn from(stats: &moq_net::session::Stats) -> Self { // An Option becomes a (value, valid) pair; absent metrics report 0/false. fn split(value: Option) -> (u64, bool) { (value.unwrap_or(0), value.is_some()) } let (rtt_us, rtt_valid) = split(stats.rtt.map(|d| d.as_micros() as u64)); let (estimated_send_rate_bps, estimated_send_rate_valid) = split(stats.estimated_send_rate.map(moq_net::bandwidth::Rate::as_bps)); let (bytes_sent, bytes_sent_valid) = split(stats.bytes_sent); let (bytes_received, bytes_received_valid) = split(stats.bytes_received); let (bytes_lost, bytes_lost_valid) = split(stats.bytes_lost); let (packets_sent, packets_sent_valid) = split(stats.packets_sent); let (packets_received, packets_received_valid) = split(stats.packets_received); let (packets_lost, packets_lost_valid) = split(stats.packets_lost); Self { rtt_us, rtt_valid, estimated_send_rate_bps, estimated_send_rate_valid, bytes_sent, bytes_sent_valid, bytes_received, bytes_received_valid, bytes_lost, bytes_lost_valid, packets_sent, packets_sent_valid, packets_received, packets_received_valid, packets_lost, packets_lost_valid, } } } /// Initialize the library with a log level. /// /// This should be called before any other functions. /// The log_level is a string: "error", "warn", "info", "debug", "trace" /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that level is a valid pointer to level_len bytes of data. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_log_level(level: *const c_char, level_len: usize) -> i32 { ffi::enter(move || { match unsafe { ffi::parse_str(level, level_len)? } { "" => moq_tokio::Log::default(), level => moq_tokio::Log::new(Level::from_str(level)?), } .init()?; Ok(()) }) } /// Human-readable reason for the most recent failed call on the calling thread. /// /// moq-c functions return only a negative code; this exposes the matching message /// (including detail the code can't carry, e.g. which URL failed to parse or why a /// decode failed). The string is only meaningful after a call returned a negative /// code; check the code first. /// /// Returns a NUL-terminated, UTF-8 pointer valid until the next moq-c call **on the /// same thread**, or NULL if no error has been recorded on this thread. Copy it if you /// need it to outlive the next call. Errors delivered through status callbacks carry /// their code directly; read this from inside the callback to get their reason. #[unsafe(no_mangle)] pub extern "C" fn moq_error() -> *const c_char { ffi::last_error_ptr() } /// Structured protocol details for the most recent failed call on the calling thread. /// /// When that failure was a session close or stream reset, writes the scope, verbatim /// wire code, and recognized kind into `out` and returns 0. Returns a negative code /// (and leaves `out` untouched) when the last error was not a protocol failure /// (transport, not-found, a bad handle, ...). Do not parse [moq_error] for this. /// /// The values are only meaningful after a call returned a negative code; check the /// code first. Same lifetime as [moq_error]: overwritten by the next moq-c call on /// this thread. Errors delivered through status callbacks are recorded before the /// callback runs, so read this from inside the callback. /// /// # Safety /// - The caller must ensure that `out` is a valid pointer to a [moq_protocol_error]. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_error_protocol(out: *mut moq_protocol_error) -> i32 { // Do not go through `enter`: a miss must not overwrite the last error we are inspecting. if out.is_null() { return Error::InvalidPointer.code(); } if ffi::last_protocol(unsafe { &mut *out }) { 0 } else { Error::NotFound.code() } } /// The protocol version names this build offers by default, spelled the way /// [moq_client_config]'s `versions` expects. Built once; the slices are valid for the life of /// the process. static VERSION_NAMES: std::sync::LazyLock> = std::sync::LazyLock::new(|| moq_net::Versions::all().iter().map(|v| v.to_string()).collect()); /// List the protocol versions offered during the handshake by default. /// /// Writes up to `count` names into `dst` and returns the total number available, which /// may be larger than `count`. Pass a NULL `dst` with a zero `count` to size the array /// first. Each name borrows a static string valid for the life of the process, so a /// caller building a menu can hold them indefinitely. /// /// Work-in-progress versions are omitted, since they are not advertised unless pinned; /// a dial still accepts them by name. /// /// Returns the total count on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that `dst` is either NULL with a zero `count`, or a valid /// pointer to `count` writable [moq_string] values. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_versions(dst: *mut moq_string, count: usize) -> i32 { ffi::enter(move || { if !dst.is_null() { let dst = unsafe { std::slice::from_raw_parts_mut(dst, count) }; for (slot, name) in dst.iter_mut().zip(VERSION_NAMES.iter()) { slot.data = name.as_ptr().cast::(); slot.len = name.len(); } } else if count != 0 { return Err(Error::InvalidPointer); } Ok(VERSION_NAMES.len()) }) } /// Whether this build can capture qlog traces. /// /// Capture is compile-time optional. [moq_client_config]'s `quic_qlog` accepts a directory /// either way, but dialing fails when the support is absent, so a caller offering the /// knob should hide it rather than surface an option that cannot work. #[unsafe(no_mangle)] pub extern "C" fn moq_qlog_supported() -> bool { moq_tokio::qlog_supported() } /// A duration as microseconds, saturating rather than wrapping. fn micros(duration: std::time::Duration) -> u64 { duration.as_micros().min(u64::MAX as u128) as u64 } /// Settings for [moq_session_connect], or NULL to dial with the defaults. /// /// Zero it (`memset`, or a `{0}` initializer) and set only what you need: a /// zeroed struct means the defaults throughout. That is why the knobs whose /// default is not zero carry a `has_*` flag rather than being read directly. The /// WebSocket fallback is on by default and the reconnect backoff starts at one /// second, so a caller who never touched them would otherwise silently turn them /// off. /// /// New settings are appended to the end of this struct, and a zeroed one keeps /// the previous behavior, so adding one does not disturb existing callers. #[repr(C)] pub struct moq_client_config { /// Protocol versions to offer during the handshake, most preferred first. /// NULL/0 offers everything this build supports. Names are spelled the way /// the CLI spells them (`moq-lite-05`, `moq-transport-22`); [moq_versions] /// lists what is on offer. pub versions: *const moq_string, /// The number of entries in `versions`. pub versions_len: usize, /// Local socket address to bind, or NULL for the wildcard address. pub bind: *const c_char, /// The length in bytes of `bind`. pub bind_len: usize, /// How long a dial may take before it gives up. pub connect_timeout_us: u64, /// Whether `connect_timeout_us` is present. pub has_connect_timeout: bool, /// Happy Eyeballs: how long before the next address is also dialed. pub failover_delay_us: u64, /// Whether `failover_delay_us` is present. pub has_failover_delay: bool, /// Happy Eyeballs: how long the first family waits for the AAAA answer. pub resolution_delay_us: u64, /// Whether `resolution_delay_us` is present. pub has_resolution_delay: bool, /// Whether the WebSocket fallback may be raced, for a UDP-blocked network. /// Enabled unless you turn it off, hence the flag. pub websocket_enabled: bool, /// Whether `websocket_enabled` is present. pub has_websocket_enabled: bool, /// How long QUIC gets before the WebSocket fallback is also dialed. pub websocket_delay_us: u64, /// Whether `websocket_delay_us` is present. pub has_websocket_delay: bool, /// Accept any certificate. Development only: prefer `tls_fingerprints`, /// and pairing this with a fingerprint or a root is rejected at dial. pub tls_disable_verify: bool, /// Whether to trust the platform root store. Its default depends on the /// backend, so it needs the flag to distinguish "off" from "unset". pub tls_system_roots: bool, /// Whether `tls_system_roots` is present. pub has_tls_system_roots: bool, /// Extra root certificate paths to trust. pub tls_roots: *const moq_string, /// The number of entries in `tls_roots`. pub tls_roots_len: usize, /// SHA-256 certificate fingerprints to pin, hex encoded. The native /// equivalent of the browser's `serverCertificateHashes`. pub tls_fingerprints: *const moq_string, /// The number of entries in `tls_fingerprints`. pub tls_fingerprints_len: usize, /// SNI override, or NULL to use the host from the URL. pub tls_host_name: *const c_char, /// The length in bytes of `tls_host_name`. pub tls_host_name_len: usize, /// Client certificate and key paths for mTLS, or NULL for none. pub tls_cert: *const c_char, /// The length in bytes of `tls_cert`. pub tls_cert_len: usize, /// The client private key path for mTLS, or NULL for none. pub tls_key: *const c_char, /// The length in bytes of `tls_key`. pub tls_key_len: usize, /// The first reconnect delay in microseconds; zero retries at once. pub backoff_initial_us: u64, /// Whether `backoff_initial_us` is present. pub has_backoff_initial: bool, /// The factor by which the reconnect delay increases; 0 or 1 keeps it constant. pub backoff_multiplier: u32, /// Whether `backoff_multiplier` is present. pub has_backoff_multiplier: bool, /// The maximum reconnect delay in microseconds; zero leaves it uncapped. pub backoff_max_us: u64, /// Whether `backoff_max_us` is present. pub has_backoff_max: bool, /// How long reconnection keeps trying before giving up for good. pub backoff_timeout_us: u64, /// Whether `backoff_timeout_us` is present. pub has_backoff_timeout: bool, /// QUIC transport tuning, all ignored by the WebSocket fallback. pub quic_max_streams: u64, /// Whether `quic_max_streams` is present. pub has_quic_max_streams: bool, /// The QUIC idle timeout in microseconds. pub quic_idle_timeout_us: u64, /// Whether `quic_idle_timeout_us` is present. pub has_quic_idle_timeout: bool, /// The QUIC keep-alive interval in microseconds. pub quic_keep_alive_us: u64, /// Whether `quic_keep_alive_us` is present. pub has_quic_keep_alive: bool, /// Generic segmentation offload and path MTU discovery. Both default to the /// backend's choice, so both need their flag. pub quic_gso: bool, /// Whether `quic_gso` is present. pub has_quic_gso: bool, /// Whether QUIC may discover the path MTU. pub quic_mtu_discovery: bool, /// Whether `quic_mtu_discovery` is present. pub has_quic_mtu_discovery: bool, /// Congestion control family name (`loss`, `delay`, or `none`), or NULL for the backend's choice. pub quic_congestion_control: *const c_char, /// The length in bytes of `quic_congestion_control`. pub quic_congestion_control_len: usize, /// Directory to write qlog traces into, or NULL for none. Capture is /// compile-time optional; see [moq_qlog_supported]. pub quic_qlog: *const c_char, /// The length in bytes of `quic_qlog`. pub quic_qlog_len: usize, /// Danger: write TLS session secrets to the file `SSLKEYLOGFILE` names, for /// decrypting a capture. Off when false; on without `SSLKEYLOGFILE` fails the dial. pub quic_keylog: bool, /// Your own `name/version` product tokens, space-separated, advertised in SETUP /// ahead of this library's (`my-app/1.0 moq-c/0.7.0 moq-net-rs/0.3.10`), or NULL for none. pub implementation: *const c_char, /// The length in bytes of `implementation`. pub implementation_len: usize, /// Credentials every dial presents, or NULL for none. The first is the connection /// credential, sent in SETUP and, while an offered version can't carry it there (lite /// before 07), as the URL's `?jwt=`; each other token widens the grant over in-band /// AUTH. A `?jwt=` already in the URL stays the connection credential, and then every /// token rides AUTH. An empty token fails the dial. pub tokens: *const moq_string, /// The number of entries in `tokens`. pub tokens_len: usize, } /// The settings [moq_session_connect] dials with when given NULL. /// /// Behaviorally the same as a zeroed struct, so this is for display rather than /// for dialing: a settings UI can show the real numbers instead of hardcoding /// ones that go stale when a default is retuned. The knobs whose default depends /// on the backend (GSO, path MTU discovery, congestion control, the TLS root /// store) come back with their `has_*` flag false, since there is no single value /// to report. /// /// Returned by value because there is nothing to fail: no handle to look up and /// no pointer to reject. Prefer a zeroed struct when you only mean to set a knob /// or two, and this when you want to read the numbers. #[unsafe(no_mangle)] pub extern "C" fn moq_client_defaults() -> moq_client_config { // SAFETY: every field is a scalar or a raw pointer, so all-zero is a valid // value, and it is the one that means "unset" throughout. let mut dst: moq_client_config = unsafe { std::mem::zeroed() }; // A panic here would have no way to report itself, so fall back to the zeroed // struct: it is what "the defaults" means to a dial anyway, and only the // reported numbers would be wrong. let filled = std::panic::catch_unwind(|| { let mut dst: moq_client_config = unsafe { std::mem::zeroed() }; let config = crate::client::Config::default(); let connect = config.connect.resolve(); dst.connect_timeout_us = micros(connect.timeout); dst.has_connect_timeout = true; dst.failover_delay_us = micros(connect.race); dst.has_failover_delay = true; dst.resolution_delay_us = micros(connect.resolution_delay); dst.has_resolution_delay = true; let websocket = config.connect.websocket.resolve(); dst.websocket_enabled = websocket.enabled; dst.has_websocket_enabled = true; dst.websocket_delay_us = micros(websocket.delay); dst.has_websocket_delay = true; dst.backoff_initial_us = micros(config.connect.backoff.initial()); dst.has_backoff_initial = true; dst.backoff_multiplier = config.connect.backoff.multiplier; dst.has_backoff_multiplier = true; dst.backoff_max_us = micros(config.connect.backoff.max()); dst.has_backoff_max = true; dst.backoff_timeout_us = micros(config.connect.backoff.timeout()); dst.has_backoff_timeout = true; let quic = config.quic.resolve(); dst.quic_max_streams = quic.max_streams; dst.has_quic_max_streams = true; dst.quic_idle_timeout_us = micros(quic.idle_timeout); dst.has_quic_idle_timeout = true; if let Some(keep_alive) = quic.keep_alive { dst.quic_keep_alive_us = micros(keep_alive); dst.has_quic_keep_alive = true; } dst }); if let Ok(value) = filled { dst = value; } dst } /// Resolve handles under the global lock, prepare the client without it, then insert /// the ready session under a short second lock. unsafe fn connect_session( url: *const c_char, url_len: usize, config: *const moq_client_config, origin_publish: u32, origin_consume: u32, on_state: ffi::moq_state_callback, user_data: *mut c_void, ) -> Result { let url = ffi::parse_url(url, url_len)?; let origin_publish = ffi::parse_id_optional(origin_publish)?; let origin_consume = ffi::parse_id_optional(origin_consume)?; // Parse before taking the lock: it validates, and a rejected value should not // have blocked every other call while it was being read. let config = unsafe { crate::parse_client(config.as_ref())? }; let (publish, consume) = { let state = State::lock(); let publish = origin_publish.map(|id| state.origin.get(id)).transpose()?.cloned(); let consume = origin_consume.map(|id| state.origin.get(id)).transpose()?.cloned(); (publish, consume) }; let callback = unsafe { ffi::OnState::new(user_data, on_state)? }; let request = Connect { config, url, publish, consume, callback, } .prepare()?; State::lock().session.connect(request) } /// Start establishing a connection to a MoQ server. /// /// Takes origin handles, which are used for publishing and consuming broadcasts respectively. /// - Any broadcasts in `origin_publish` will be announced to the server. /// - Any broadcasts announced by the server will be available in `origin_consume`. /// - If an origin handle is 0, that functionality is completely disabled. /// /// This may be called multiple times to connect to different servers. /// Origins can be shared across sessions, useful for fanout or relaying. /// /// Pass NULL for `config` to dial with the defaults. Fill in a /// [moq_client_config] to pin a protocol version, adjust TLS trust, or tune the /// transport; it is read during the call and not retained, so the same one can /// dial any number of sessions. /// /// Returns a non-zero handle to the session on success, or a negative code on (immediate) failure. /// You should call [moq_session_close], even on error, to free up resources. /// /// The session reconnects automatically with exponential backoff if the connection drops. /// Published broadcasts are re-announced and consumers re-subscribed on each reconnect, /// since the origins outlive the underlying connection. /// /// `on_state` receives a [moq_connection_state] whenever the lifecycle changes: each /// connect, drop, migration, and failed dial, carrying the phase, the connect count, and /// why the latest attempt failed. Changes that land together arrive as one snapshot. /// /// The last call has phase `MOQ_CONNECTION_PHASE_CLOSED`, with `failure` zero after /// [moq_session_close] or the negative code that made reconnecting give up. By then the /// session handle is already released, and `on_state` and `user_data` are never touched /// again, so that terminal snapshot is the point to release `user_data`. It fires even /// after [moq_session_close], so do not free `user_data` on the close call itself. /// /// # Safety /// - The caller must ensure that url is a valid pointer to url_len bytes of data. /// - `config` must be NULL, or an aligned, readable [moq_client_config]. Every /// non-NULL pointer inside it must be valid for its paired length, and all of /// them must stay alive for the duration of this call: the config is read /// here, not copied by whoever filled it in. /// - The caller must keep `user_data` valid until the terminal `on_state` callback. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_session_connect( url: *const c_char, url_len: usize, config: *const moq_client_config, origin_publish: u32, origin_consume: u32, on_state: ffi::moq_state_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || unsafe { connect_session( url, url_len, config, origin_publish, origin_consume, on_state, user_data, ) }) } /// Request that a session shut down. /// /// Returns immediately: zero on success, or a negative code if the session is /// unknown or already closing. Does NOT free `user_data`. The session's /// `on_state` callback still fires once more with its terminal snapshot once the /// close has reached the peer, after at most a second of draining, and that final /// callback is where `user_data` should be released. Safe to call from any thread, /// including from within `on_state`. #[unsafe(no_mangle)] pub extern "C" fn moq_session_close(session: u32) -> i32 { ffi::enter(move || { let session = ffi::parse_id(session)?; State::lock().session.close(session) }) } /// Snapshot the current connection statistics for a session. /// /// Fills `dst` with a point-in-time view of the underlying QUIC/WebTransport connection /// (RTT, bandwidth estimates, byte/packet counters). Each metric carries a `*_valid` flag /// since availability depends on the transport backend; see [moq_connection_stats]. /// /// Returns zero on success, or a negative code on failure: the session handle is unknown, or /// the session is currently reconnecting and has no live connection (in which case `dst` is /// left untouched). Safe to call repeatedly to poll stats over the life of the session. /// /// # Safety /// - The caller must ensure that `dst` is a valid pointer to a [moq_connection_stats] struct. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_session_stats(session: u32, dst: *mut moq_connection_stats) -> i32 { ffi::enter(move || { let session = ffi::parse_id(session)?; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; let stats = State::lock().session.stats(session)?; *dst = moq_connection_stats::from(&stats); Ok(()) }) } /// Snapshot statistics and the negotiated protocol from the same live connection. /// /// Returns zero on success, or a negative code when the handle is unknown or offline /// between reconnects. On failure, `dst` is untouched. The protocol string points at /// static storage valid for the process lifetime and must not be freed. /// /// # Safety /// - `dst` must point at a writable [moq_connection_snapshot] struct. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_session_snapshot(session: u32, dst: *mut moq_connection_snapshot) -> i32 { ffi::enter(move || { let session = ffi::parse_id(session)?; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; let (stats, version) = State::lock().session.snapshot(session)?; let name = version.as_str(); *dst = moq_connection_snapshot { stats: moq_connection_stats::from(&stats), protocol: moq_string { data: name.as_ptr().cast::(), len: name.len(), }, }; Ok(()) }) } /// Settings for [moq_server_listen]. /// /// Zero it and set only what you need; new settings are appended, and a zeroed /// one keeps the previous behavior. TLS is required: set `tls_cert` and `tls_key`, /// or `tls_generate`. #[repr(C)] pub struct moq_server_config { /// Address to bind, e.g. `[::]:443`, `127.0.0.1:0`, or `localhost:4443`; NULL /// for `[::]:443`. A port of 0 picks one; read it back with [moq_server_addr]. pub bind: *const c_char, /// The length in bytes of `bind`. pub bind_len: usize, /// Certificate chain paths (PEM), paired with `tls_key`. pub tls_cert: *const moq_string, /// The number of entries in `tls_cert`. pub tls_cert_len: usize, /// Private key paths (PEM), paired with `tls_cert`. pub tls_key: *const moq_string, /// The number of entries in `tls_key`. pub tls_key_len: usize, /// Hostnames to generate a self-signed certificate for. Clients must pin its /// fingerprint ([moq_server_fingerprints]) or disable verification. pub tls_generate: *const moq_string, /// The number of entries in `tls_generate`. pub tls_generate_len: usize, /// Danger: write TLS session secrets to the file `SSLKEYLOGFILE` names, for /// decrypting a capture. Off when false; on without `SSLKEYLOGFILE` fails the listen. pub quic_keylog: bool, /// Your own `name/version` product tokens, advertised in SETUP ahead of this /// library's, like [moq_client_config]'s `implementation`. NULL for none. pub implementation: *const c_char, /// The length in bytes of `implementation`. pub implementation_len: usize, } /// Build the listener `config` describes, binding its socket. /// /// # Safety /// - Every non-NULL pointer in `config` must be valid for its length. unsafe fn parse_server(config: &moq_server_config) -> Result { let mut listen = moq_tokio::listen::Config::default(); let prefix = unsafe { ffi::parse_str_optional(config.implementation, config.implementation_len)? }; listen.implementation = Some(crate::client::implementation(prefix)?); if let Some(bind) = unsafe { ffi::parse_str_optional(config.bind, config.bind_len)? } { let bind = moq_tokio::listen::Bind::from_str(bind) .map_err(|_| Error::InvalidConfig(format!("invalid bind address: {bind}")))?; listen.bind = Some(bind); } listen.tls.cert = unsafe { ffi::parse_strings(config.tls_cert, config.tls_cert_len)? } .into_iter() .map(Into::into) .collect(); listen.tls.key = unsafe { ffi::parse_strings(config.tls_key, config.tls_key_len)? } .into_iter() .map(Into::into) .collect(); listen.tls.generate = unsafe { ffi::parse_strings(config.tls_generate, config.tls_generate_len)? }; let mut quic = moq_tokio::quic::Config::default(); quic.keylog = config .quic_keylog .then(|| std::env::var_os("SSLKEYLOGFILE").unwrap_or_default().into()); listen.init(quic).map_err(|err| Error::InvalidConfig(err.to_string())) } /// Listen for incoming sessions. /// /// Binds before returning, so a bad address or certificate fails here with a reason in /// [moq_error]. Returns a non-zero server handle on success, or a negative code on failure. /// /// `on_request` is called with a positive session request handle for each incoming /// session, then exactly once more with a terminal code: `0` (stopped cleanly, including /// after [moq_server_close]) or a negative error. After the terminal (`<= 0`) callback, /// `user_data` is never touched again, so release it there. Answer each request with /// [moq_session_request_accept], [moq_session_request_reject], or [moq_session_request_free]. /// /// # Safety /// - `config` must point at a readable [moq_server_config] whose non-NULL pointers are /// valid for their paired lengths during this call. /// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_request` callback. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_server_listen( config: *const moq_server_config, on_request: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?; let on_request = unsafe { ffi::OnStatus::new(user_data, on_request)? }; // Bind without the global lock: resolving and loading certificates can be slow. let server = unsafe { parse_server(config)? }; State::lock().server.listen(server, on_request) }) } /// The address a server bound, e.g. `127.0.0.1:4443`. /// /// The destination borrows the server's storage, valid until its terminal /// `on_request` callback. Returns a zero on success, or a negative code on failure. /// /// # Safety /// - `dst` must point at a writable [moq_string]. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_server_addr(server: u32, dst: *mut moq_string) -> i32 { ffi::enter(move || { let server = ffi::parse_id(server)?; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; State::lock().server.addr(server, dst) }) } /// The SHA-256 fingerprints of the server's certificates, hex encoded. /// /// Pin these on a client (`tls_fingerprints` in [moq_client_config], or a browser's /// `serverCertificateHashes`) to trust a generated certificate. Writes up to `count` /// into `dst` and returns the total number available; pass a NULL `dst` with a zero /// `count` to size the array first. Each string borrows the server's storage, valid /// until its terminal `on_request` callback. /// /// Returns the total count on success, or a negative code on failure. /// /// # Safety /// - `dst` must be NULL with a zero `count`, or point to `count` writable [moq_string] values. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_server_fingerprints(server: u32, dst: *mut moq_string, count: usize) -> i32 { ffi::enter(move || { let server = ffi::parse_id(server)?; let dst = if count == 0 { &mut [][..] } else { if dst.is_null() { return Err(Error::InvalidPointer); } unsafe { std::slice::from_raw_parts_mut(dst, count) } }; State::lock().server.fingerprints(server, dst) }) } /// Stop listening. /// /// Returns immediately: zero on success, or a negative code if the server is unknown or /// already closing. Sessions already accepted keep running. The `on_request` callback /// still fires once more with a terminal `0` after the sockets are released, so the /// address can be bound again from there; release `user_data` in that callback. #[unsafe(no_mangle)] pub extern "C" fn moq_server_close(server: u32) -> i32 { ffi::enter(move || { let server = ffi::parse_id(server)?; State::lock().server.close(server) }) } /// The path of a session request, without the query, or empty for the root. /// /// The destination borrows the request's storage: copy it out before accept, /// reject, or [moq_session_request_free]. Returns a zero on success, or a negative /// code on failure. /// /// # Safety /// - `dst` must point at a writable [moq_string]. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_session_request_path(request: u32, dst: *mut moq_string) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; State::lock().server.request_path(request, dst) }) } /// The query of a session request without the leading `?`, or a NULL `data` if it has none. /// /// Where a token usually rides. The destination borrows the request's storage, like /// [moq_session_request_path]. Returns a zero on success, or a negative code on failure. /// /// # Safety /// - `dst` must point at a writable [moq_string]. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_session_request_query(request: u32, dst: *mut moq_string) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; State::lock().server.request_query(request, dst) }) } /// The implementation the client advertised in its SETUP, such as /// `moq-c/0.7.0 moq-net-rs/0.3.10`, or a NULL `data` if it advertised none. /// /// Peer-controlled: use it for logs and rollout counts, never to authorize. The /// destination borrows the request's storage, like [moq_session_request_path]. /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - `dst` must point at a writable [moq_string]. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_session_request_peer_implementation(request: u32, dst: *mut moq_string) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; State::lock().server.request_peer_implementation(request, dst) }) } /// Accept a session request, completing the MoQ handshake. /// /// Takes origin handles like [moq_session_connect]: broadcasts in `origin_publish` are /// announced to the peer, and broadcasts the peer announces land in `origin_consume`. /// An origin handle of 0 disables that direction. /// /// Consumes the request handle on success and returns a non-zero session handle, or a /// negative code on failure (leaving the request unanswered). The session works with every /// `moq_session_*` call; stats and bandwidth report offline until the handshake completes. /// /// `on_state` reports the lifecycle like [moq_session_connect]'s: one connected snapshot /// once the handshake completes, since an accepted session is a single connection that /// never reconnects, then the terminal closed snapshot. Its `failure` is zero only after /// [moq_session_close], and otherwise the negative code of a failed handshake or the /// peer's close; read [moq_error_protocol] during the callback for the peer's close code. /// /// # Safety /// - The caller must keep `user_data` valid until the terminal `on_state` callback. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_session_request_accept( request: u32, origin_publish: u32, origin_consume: u32, on_state: ffi::moq_state_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; let origin_publish = ffi::parse_id_optional(origin_publish)?; let origin_consume = ffi::parse_id_optional(origin_consume)?; let callback = unsafe { ffi::OnState::new(user_data, on_state)? }; let mut state = State::lock(); let publish = origin_publish.map(|id| state.origin.get(id)).transpose()?.cloned(); let consume = origin_consume.map(|id| state.origin.get(id)).transpose()?.cloned(); let request = state.server.request_take(request)?; state.session.accept(request, publish, consume, callback) }) } /// Reject a session request with an HTTP-style status code. /// /// 401 and 403 are sent as the protocol's unauthorized close; every other code is sent /// as an application error. Consumes the request handle. Returns a zero on success, or /// a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_session_request_reject(request: u32, code: u16) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; let request = State::lock().server.request_take(request)?; let reject = match code { 401 | 403 => moq_net::SessionError::Unauthorized, code => moq_net::SessionError::App(code), }; // Rejecting only queues the close, so this never waits on the network. Ok::<_, Error>(pollster::block_on(request.reject(reject))?) }) } /// Free a session request without accepting or rejecting it. /// /// Dropping the request closes the session. Returns a zero on success, or a negative /// code if the handle is unknown. #[unsafe(no_mangle)] pub extern "C" fn moq_session_request_free(request: u32) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; State::lock().server.request_take(request)?; Ok(()) }) } /// Create an origin for publishing broadcasts. /// /// Origins contain any number of broadcasts addressed by path. /// The same broadcast can be published to multiple origins under different paths. /// /// [moq_origin_announced] can be used to discover broadcasts published to this origin. /// This is extremely useful for discovering what is available on the server to [moq_origin_request]. /// /// Returns a non-zero handle to the origin on success. #[unsafe(no_mangle)] pub extern "C" fn moq_origin_create() -> i32 { ffi::enter(move || State::lock().origin.create()) } /// Create a broadcast at `path` on an origin, for publishing media tracks. /// /// The broadcast is invisible and unroutable, on this origin and its peers /// alike, until [moq_publish_announce]. Fill it with the `moq_publish_*` /// functions, then announce it. [moq_publish_close] ends it for good. /// /// Returns a non-zero broadcast handle on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that path is a valid pointer to path_len bytes of data. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_origin_create_broadcast(origin: u32, path: *const c_char, path_len: usize) -> i32 { ffi::enter(move || { let origin = ffi::parse_id(origin)?; let path = unsafe { ffi::parse_str(path, path_len)? }; let mut state = State::lock(); let broadcast = state.origin.create_broadcast(origin, path)?; state.publish.create_publication(broadcast) }) } /// Advertise `prefix` and serve the requests beneath it. /// /// A route claims `prefix` and every path beneath it (the empty prefix claims /// every path). A service that only serves some of them advertises the /// covering prefix and rejects the rest as they are requested. `on_request` is /// required: a NULL callback is refused before the route is advertised. It is /// invoked with a positive request handle for each /// pending broadcast, then exactly once more with a terminal code: `0` (stopped /// cleanly, including after [moq_origin_requests_cancel]) or a negative error. /// After the terminal (`<= 0`) callback, `user_data` is never touched again. /// /// Returns a non-zero handle on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that prefix is a valid pointer to prefix_len bytes of data. /// - `on_request` must be non-NULL; a missing callback is refused before the route is advertised. /// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_request` callback. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_origin_requests( origin: u32, prefix: *const c_char, prefix_len: usize, on_request: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let origin = ffi::parse_id(origin)?; let prefix = unsafe { ffi::parse_str(prefix, prefix_len)? }; let on_request = unsafe { ffi::OnStatus::new(user_data, on_request)? }; State::lock().origin.dynamic(origin, prefix, on_request) }) } /// Stop serving and retract the route. /// /// Returns immediately: zero on success, or a negative code if already closed. /// The [moq_origin_requests] `on_request` callback still fires once more with a /// terminal `0` (or a negative error), and that final callback is where /// `user_data` should be released. #[unsafe(no_mangle)] pub extern "C" fn moq_origin_requests_cancel(dynamic: u32) -> i32 { ffi::enter(move || { let dynamic = ffi::parse_id(dynamic)?; State::lock().origin.dynamic_close(dynamic) }) } /// The path of a broadcast request delivered to a [moq_origin_requests] callback. /// /// The destination borrows the request's storage: copy it out before accept, /// reject, or [moq_broadcast_request_free]. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - `dst` must point at a writable [moq_string]. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_broadcast_request_path(request: u32, dst: *mut moq_string) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; State::lock().origin.broadcast_request_path(request, dst) }) } /// Start a broadcast request answer under `epoch`, hidden until published. /// /// Consumes the request handle, writes the new broadcast handle to `broadcast` /// (which cannot announce itself; only [moq_broadcast_prepared_publish] does), /// and returns a positive prepared-answer handle or a negative error. /// /// # Safety /// - `epoch` must point at `epoch_len` readable bytes containing a UUIDv7 epoch. /// - `broadcast` must point at writable storage for one handle. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_broadcast_request_prepare( request: u32, epoch: *const c_char, epoch_len: usize, broadcast: *mut u32, ) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; let epoch = unsafe { ffi::parse_str(epoch, epoch_len)? } .parse() .map_err(|_| Error::InvalidCode)?; let broadcast_out = unsafe { broadcast.as_mut() }.ok_or(Error::InvalidPointer)?; let mut state = State::lock(); let (pending, driver) = state.origin.broadcast_request_take(request)?; let prepared = pending.prepare(epoch)?; let broadcast = state.publish.create((prepared.broadcast().clone(), driver.clone()))?; let id = state.origin.prepared_insert(prepared, driver, broadcast)?; *broadcast_out = broadcast.into(); Ok(id) }) } /// Publish a prepared claim answer and resolve every waiting requester. /// /// Consumes the prepared handle. The broadcast handle returned by /// [moq_broadcast_request_prepare] remains owned by the caller. #[unsafe(no_mangle)] pub extern "C" fn moq_broadcast_prepared_publish(prepared: u32) -> i32 { ffi::enter(move || { let prepared = ffi::parse_id(prepared)?; let mut state = State::lock(); let (prepared, driver, broadcast) = state.origin.prepared_take(prepared)?; driver.check()?; let result = state.publish.admit(broadcast, prepared); driver.check()?; result }) } /// Free a prepared claim answer without publishing it. #[unsafe(no_mangle)] pub extern "C" fn moq_broadcast_prepared_free(prepared: u32) -> i32 { ffi::enter(move || { let prepared = ffi::parse_id(prepared)?; State::lock().origin.prepared_take(prepared)?; Ok(()) }) } /// Reject a broadcast request with an application error code. /// /// Consumes the request handle. Returns a zero on success, or a negative code /// on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_broadcast_request_reject(request: u32, error_code: u16) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; let (pending, driver) = State::lock().origin.broadcast_request_take(request)?; driver.check()?; pending.reject(moq_net::Error::App(error_code)); driver.check()?; Ok(()) }) } /// Free a broadcast request without accepting or rejecting it. /// /// Dropping the request rejects it. Returns a zero on success, or a negative /// code if the handle is unknown. #[unsafe(no_mangle)] pub extern "C" fn moq_broadcast_request_free(request: u32) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; State::lock().origin.broadcast_request_take(request)?; Ok(()) }) } /// Learn about broadcasts matching a pattern scope under an origin. /// /// `config` selects the paths; NULL lists every visible one. Delivered /// [moq_announce_event] prefixes remain relative to the origin. /// /// `on_announce` is invoked with a positive announced ID for each event (see /// [moq_announce_kind]), then exactly once more with a terminal code: `0` (stopped cleanly) or a /// negative error. After the terminal (`<= 0`) callback, `on_announce` is never /// called again and `user_data` is never touched again, so release `user_data` /// there. The terminal callback fires even after [moq_origin_announced_cancel]. /// /// - [moq_origin_announced_info] is used to query information about the broadcast. /// - [moq_origin_announced_free] releases each delivered announced ID once read. /// - [moq_origin_announced_cancel] is used to stop receiving announcements. /// /// Returns a non-zero handle on success, or a negative code on failure. /// /// # Safety /// - `config` may be NULL, or must point at a readable [moq_announce_config] /// whose strings are valid for their lengths. /// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_announce` callback. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_origin_announced( origin: u32, config: *const moq_announce_config, on_announce: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let origin = ffi::parse_id(origin)?; let (scope, hidden) = match unsafe { config.as_ref() } { None => (moq_net::Pattern::all(), false), Some(config) => { let prefix = unsafe { ffi::parse_str(config.prefix, config.prefix_len)? }; let filter = if config.filter.is_null() { moq_net::Pattern::all() } else { unsafe { ffi::parse_str(config.filter, config.filter_len)? }.parse()? }; (filter.rooted(prefix)?, config.hidden) } }; let on_announce = unsafe { ffi::OnStatus::new(user_data, on_announce)? }; State::lock().origin.announced(origin, scope, hidden, on_announce) }) } /// Query information about a broadcast discovered by [moq_origin_announced]. /// /// The destination is filled with the route information. The `prefix`, `captures`, /// and capture string pointers borrow the announcement's storage: copy them out /// before calling [moq_origin_announced_free], which invalidates them. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that `dst` is a valid pointer to a [moq_announce_event] struct. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_origin_announced_info(announced: u32, dst: *mut moq_announce_event) -> i32 { ffi::enter(move || { let announced = ffi::parse_id(announced)?; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; State::lock().origin.announced_info(announced, dst) }) } /// Free a single announcement delivered to a [moq_origin_announced] `on_announce` callback. /// /// Each event hands the callback a distinct announcement handle (read /// with [moq_origin_announced_info]); release it here once done to avoid leaking one per event /// over the life of the listener. This is per-announcement and distinct from /// [moq_origin_announced_cancel], which stops the listener itself. After freeing, /// any pointer obtained from [moq_origin_announced_info] for this handle is dangling. /// /// Returns zero on success, or a negative code if the handle is unknown. #[unsafe(no_mangle)] pub extern "C" fn moq_origin_announced_free(announced: u32) -> i32 { ffi::enter(move || { let announced = ffi::parse_id(announced)?; State::lock().origin.announced_free(announced) }) } /// Stop receiving announcements for broadcasts published to an origin. /// /// Returns immediately: zero on success, or a negative code if already closed. /// Does NOT free `user_data`. The [moq_origin_announced] `on_announce` callback /// still fires once more with a terminal `0` (or a negative error), and that /// final callback is where `user_data` should be released. #[unsafe(no_mangle)] pub extern "C" fn moq_origin_announced_cancel(announced: u32) -> i32 { ffi::enter(move || { let announced = ffi::parse_id(announced)?; State::lock().origin.announced_close(announced) }) } /// Follow a broadcast on an origin by path, delivering each publisher instance that serves it. /// /// Resolves against future announcements: it waits for the announcement to arrive (e.g. over the /// network) and then delivers the broadcast handle via `on_broadcast`. Use it right after /// [moq_session_connect] to avoid racing announcement gossip. To resolve against only what is /// reachable now, use [moq_origin_request] instead. A broadcast created on this origin /// resolves once it is announced, like a remote one. /// /// `on_broadcast` is invoked with a positive broadcast handle once announced, and again with a /// new handle each time another publisher instance takes over (a restarted publisher, or a /// replacement session) or the broadcast returns after it went offline. Drop what was read /// from the previous handle then: its tracks end when its session does. The wait ends with /// exactly one terminal code: `0` (after [moq_origin_announced_broadcast_cancel], or the origin /// closing once a handle was delivered) or a negative error. After the terminal (`<= 0`) callback, /// `on_broadcast` is never called again and `user_data` is never touched again, so release /// `user_data` there. The broadcast handle is usable with [moq_consume_catalog] / [moq_consume_track] /// and must be freed separately with [moq_consume_close]. /// /// Returns a non-zero handle to the wait on success, or a negative code on (immediate) failure. /// /// # Safety /// - The caller must ensure that path is a valid pointer to path_len bytes of data. /// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_broadcast` callback. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_origin_announced_broadcast( origin: u32, path: *const c_char, path_len: usize, on_broadcast: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let origin = ffi::parse_id(origin)?; let path = unsafe { ffi::parse_str(path, path_len)? }.to_string(); let on_broadcast = unsafe { ffi::OnStatus::new(user_data, on_broadcast)? }; State::lock().origin.consume_announced(origin, path, on_broadcast) }) } /// Abort a wait started by [moq_origin_announced_broadcast]. /// /// Returns immediately: zero on success, or a negative code if already closed. Does NOT free /// `user_data`. The [moq_origin_announced_broadcast] `on_broadcast` callback still fires once more /// with a terminal `0` (or a negative error), and that final callback is where `user_data` should /// be released. Any broadcast handle already delivered is unaffected and must still be freed with /// [moq_consume_close]. #[unsafe(no_mangle)] pub extern "C" fn moq_origin_announced_broadcast_cancel(task: u32) -> i32 { ffi::enter(move || { let task = ffi::parse_id(task)?; State::lock().origin.consume_announced_close(task) }) } /// Request a broadcast from an origin by path, resolving as soon as it can be served. /// /// Resolves against what is announced *now*, where [moq_origin_announced_broadcast] waits /// indefinitely: it returns an announced broadcast at once, and fails when none is reachable, /// including a broadcast created but not announced. It does NOT wait for a later /// announcement. Serve on-demand paths with [moq_origin_requests]. A client session only /// asks its peer about paths something watches, through [moq_origin_announced] or /// [moq_origin_announced_broadcast], so a path nothing watches is unreachable on it. /// /// `on_broadcast` is invoked with a positive broadcast handle once served, then exactly once more /// with a terminal code: `0` (finished, including after [moq_origin_request_cancel]) or a negative /// error. After the terminal (`<= 0`) callback, `user_data` is never touched again, so release it /// there. The broadcast handle is usable with [moq_consume_catalog] / [moq_consume_track] and must /// be freed separately with [moq_consume_close]. /// /// Returns a non-zero handle to the request on success, or a negative code on (immediate) failure. /// /// # Safety /// - The caller must ensure that path is a valid pointer to path_len bytes of data. /// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_broadcast` callback. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_origin_request( origin: u32, path: *const c_char, path_len: usize, on_broadcast: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let origin = ffi::parse_id(origin)?; let path = unsafe { ffi::parse_str(path, path_len)? }.to_string(); let on_broadcast = unsafe { ffi::OnStatus::new(user_data, on_broadcast)? }; State::lock().origin.request(origin, path, on_broadcast) }) } /// Abort a request started by [moq_origin_request]. /// /// Returns immediately: zero on success, or a negative code if already closed. Does NOT free /// `user_data`; the [moq_origin_request] `on_broadcast` callback fires once more with a terminal /// code, which is where `user_data` should be released. Any broadcast handle already delivered is /// unaffected and must still be freed with [moq_consume_close]. #[unsafe(no_mangle)] pub extern "C" fn moq_origin_request_cancel(task: u32) -> i32 { ffi::enter(move || { let task = ffi::parse_id(task)?; State::lock().origin.consume_announced_close(task) }) } /// Close an origin and clean up its resources. /// /// The origin keeps running while a broadcast published on it or a /// [moq_origin_requests] handler lives; close or cancel those to end it. /// /// Returns a zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_origin_close(origin: u32) -> i32 { ffi::enter(move || { let origin = ffi::parse_id(origin)?; State::lock().origin.close(origin) }) } /// Advertise a broadcast's exact path as a route. /// /// Announcing again re-prices the route in place. A NULL `route` uses the default /// (no hops, cost 0). Until announced, the broadcast is invisible and unroutable for /// local consumers and peers alike. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - `route` may be NULL, or must point at a readable [moq_route]. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_announce(broadcast: u32, route: *const moq_route) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let route = unsafe { parse_route(route)? }; State::lock().publish.announce(broadcast, route) }) } /// Retract a broadcast's exact-path advertisement, if any. /// /// Local consumers and peers alike stop discovering and requesting it; tracks already in /// flight carry on, and announcing again brings it back. Returns a zero on success, or a /// negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_unannounce(broadcast: u32) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; State::lock().publish.unannounce(broadcast) }) } /// End a broadcast for good and release its handle. /// /// The origin retracts the path immediately and serves no new tracks. Every media, raw track, /// container, encoder, JSON, and binary handle still open on the broadcast ends with `UNROUTABLE`, /// the way a reader sees a publisher leave, and is released; finish a track first to end it /// cleanly. The /// handles are invalid afterwards, so closing it again fails like any unknown handle, and so /// does any later call on a released child. /// /// Returns a zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_close(broadcast: u32) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; // Bound first, so the global lock is released before the encoders end. let closing = State::lock().publish_close(broadcast)?; closing.run() }) } /// Finish a broadcast's catalog, which ends its content. /// /// Finish every track first. The broadcast stays announced and keeps serving its finished /// tracks, so a subscriber that arrives late can still read them, until [moq_publish_close] /// takes it offline. How long to keep it up is yours to decide. /// /// Returns a zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_catalog_finish(broadcast: u32) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; State::lock().publish.catalog_finish(broadcast) }) } /// Publish one audio codec as a new media track. /// /// The track is named after the format (`0.opus`), so a subscriber finds it /// through the catalog rather than by a name you choose. /// [moq_audio_init::init] is required: audio resolves its whole rendition from /// those bytes. Frames written with [moq_publish_media_frame] must be in decode /// order. /// /// Returns a non-zero handle to the track on success, or a negative code on failure. /// /// # Safety /// - `config` must be NULL, or point to an aligned, readable [moq_audio_init]. /// Every non-NULL pointer inside it must be valid for its paired length and /// stay alive for the duration of this call. A NULL config is rejected with an /// ordinary error. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_audio(broadcast: u32, config: *const moq_audio_init) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let (audio, hls) = unsafe { parse_audio_init(config)? }; State::lock().publish.audio(broadcast, audio, hls) }) } /// Returns the init and the track's group duration hint. /// /// # Safety /// - As [moq_publish_audio], for `config`. unsafe fn parse_audio_init( config: *const moq_audio_init, ) -> Result<(moq_mux::import::AudioInit, Option), Error> { let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?; let init = unsafe { ffi::parse_slice(config.init, config.init_len)? }; let label = unsafe { ffi::parse_str_optional(config.label, config.label_len)? }; let mut audio = moq_mux::import::AudioInit::new(audio_format_from_u32(config.format)?, init.to_vec()); audio.label = label.map(str::to_string); config.hint.apply(&mut audio)?; Ok((audio, config.hint.group_duration()?)) } /// Publish one video codec as a new media track. /// /// Named as in [moq_publish_audio]. [moq_video_init::init] may be NULL for a /// format that resolves in band. /// /// Returns a non-zero handle to the track on success, or a negative code on failure. /// /// # Safety /// - As [moq_publish_audio], for a [moq_video_init]. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_video(broadcast: u32, config: *const moq_video_init) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let (video, hls) = unsafe { parse_video_init(config)? }; State::lock().publish.video(broadcast, video, hls) }) } /// Returns the init and the track's group duration hint. /// /// # Safety /// - As [moq_publish_audio], for a [moq_video_init]. unsafe fn parse_video_init( config: *const moq_video_init, ) -> Result<(moq_mux::import::VideoInit, Option), Error> { let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?; let init = unsafe { ffi::parse_slice(config.init, config.init_len)? }; let label = unsafe { ffi::parse_str_optional(config.label, config.label_len)? }; let mut video = moq_mux::import::VideoInit::new(video_format_from_u32(config.format)?, init.to_vec()); video.label = label.map(str::to_string); video.hint = config.hint.resolve()?; Ok((video, config.hint.group_duration()?)) } /// Publish a container, which demuxes and publishes its own tracks. /// /// Feed it whole chunks with [moq_publish_container_write]. Unlike the codec /// entry points there is no label: a container describes each track it publishes /// from its own metadata. /// /// Returns a non-zero handle to the container on success, or a negative code on failure. /// /// # Safety /// - As [moq_publish_audio], for a [moq_container_init]. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_container(broadcast: u32, config: *const moq_container_init) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?; let init = unsafe { ffi::parse_slice(config.init, config.init_len)? }; let container = moq_mux::import::ContainerInit::new(container_format_from_u32(config.format)?, init.to_vec()); State::lock().publish.container(broadcast, container) }) } /// Draw a group boundary on a media importer. /// /// For a codec track this ends the open group; the next frame written starts a new one. Audio has /// no boundary of its own (every packet is independently decodable), so this is the only thing /// that gives it groups: call it after every frame for one group (one QUIC stream) the relay /// forwards without waiting, or at a segment cadence to align with video for HLS/DASH. Video /// groups at its own keyframes and needs this only to override that. /// /// A container has its own [moq_publish_container_cut], since it rolls a group on every track it /// publishes rather than ending one group. /// /// Returns a zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_media_cut(media: u32) -> i32 { ffi::enter(move || { let media = ffi::parse_id(media)?; State::lock().publish.media_cut(media) }) } /// Draw a group boundary and number the next group `sequence`. /// /// [moq_publish_media_cut] with an explicit sequence, for a caller whose group numbers have to be /// deterministic: two encoders publishing the same content align per GOP so a consumer can fail /// over between them. /// /// Returns a zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_media_seek(media: u32, sequence: u64) -> i32 { ffi::enter(move || { let media = ffi::parse_id(media)?; State::lock().publish.media_seek(media, sequence) }) } /// Finish a media track, flushing any buffered frames. No more frames can be written. /// /// The broadcast keeps the finished track, catalog rendition included, until it closes, so a /// late subscriber can still drain it. A failed finish ends the track with its error instead. /// /// Returns a zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_media_finish(export: u32) -> i32 { ffi::enter(move || { let export = ffi::parse_id(export)?; State::lock().publish.media_finish(export) }) } /// Watch whether a media track has subscribers, so an encoder runs only while someone watches. /// /// `on_demand` fires right away with the current [moq_demand] state, again on every /// change, including a move of the wanted group that [moq_publish_demand_wanted] reads, /// then exactly once more with a terminal code: `0` (the track ended or the watcher was /// stopped with [moq_publish_demand_cancel]) or a negative error. After the terminal /// (`<= 0`) callback, `user_data` is never touched again. Reporting the current state /// first means a track that went unused before the watcher existed still reports it. /// /// A container handle is refused: it publishes several tracks and has no single demand. /// /// Returns a non-zero watcher handle on success, or a negative code on failure. /// /// # Safety /// - `on_demand` must be non-NULL. /// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_demand` callback. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_media_demand( media: u32, on_demand: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let media = ffi::parse_id(media)?; let on_demand = unsafe { ffi::OnStatus::new(user_data, on_demand)? }; let mut state = State::lock(); let demand = state.publish.media_demand(media)?; state.publish.demand(demand, on_demand) }) } /// Stop a demand watcher from [moq_publish_track_demand], [moq_publish_media_demand], /// [`crate::moq_encode_video_demand`], or [`crate::moq_encode_audio_demand`]. /// /// Returns immediately: zero on success, or a negative code if already closed. The /// watcher's `on_demand` callback still fires once more with a terminal `0`, and /// that final callback is where `user_data` should be released. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_demand_cancel(watcher: u32) -> i32 { ffi::enter(move || { let watcher = ffi::parse_id(watcher)?; State::lock().publish.demand_close(watcher) }) } /// Read the group a demand watcher's subscribers want, typically from its `on_demand` callback. /// /// `present` is false while nobody subscribes. Otherwise subscribers want `group` or later: /// once `group` passes the largest group written, start a new one (a keyframe). `group` /// never goes back, so once a group reaches it nothing is pending until a joiner asks again. /// /// Returns zero on success, or a negative code once the watcher's terminal callback fired. /// /// # Safety /// - `group` and `present` must be valid pointers. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_demand_wanted(watcher: u32, group: *mut u64, present: *mut bool) -> i32 { ffi::enter(move || { let watcher = ffi::parse_id(watcher)?; let group = unsafe { group.as_mut() }.ok_or(Error::InvalidPointer)?; let present = unsafe { present.as_mut() }.ok_or(Error::InvalidPointer)?; let wanted = State::lock().publish.demand_wanted(watcher)?; *group = wanted.unwrap_or(0); *present = wanted.is_some(); Ok(()) }) } /// Write a whole chunk of container bytes. /// /// No timestamp: a container carries its tracks' timing itself, and the importer /// reads it out rather than taking the caller's word for it. /// /// Returns zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure `payload` is valid for `payload_size` bytes. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_container_write(container: u32, payload: *const u8, payload_size: usize) -> i32 { ffi::enter(move || { let container = ffi::parse_id(container)?; let payload = unsafe { ffi::parse_slice(payload, payload_size)? }; State::lock().publish.container_write(container, payload) }) } /// Declare that the next chunk starts a new segment, rolling a group on every /// track the container publishes. /// /// An fMP4 source carrying `styp` atoms declares its own segments, so this is /// only needed when it doesn't. Formats with no segment concept (MKV, TS, FLV) /// ignore it. /// /// Returns zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_container_cut(container: u32) -> i32 { ffi::enter(move || { let container = ffi::parse_id(container)?; State::lock().publish.container_cut(container) }) } /// Start a new segment and number its groups `sequence`. /// /// Returns zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_container_seek(container: u32, sequence: u64) -> i32 { ffi::enter(move || { let container = ffi::parse_id(container)?; State::lock().publish.container_seek(container, sequence) }) } /// Finish every track the container publishes and release the handle. /// /// The broadcast keeps the finished tracks, catalog renditions included, until it closes, so a /// late subscriber can still drain them. A failed finish ends the tracks with its error instead. /// /// Returns zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_container_finish(container: u32) -> i32 { ffi::enter(move || { let container = ffi::parse_id(container)?; State::lock().publish.container_finish(container) }) } /// Write data to a track. /// /// The encoding of `data` depends on the track `format`. /// The timestamp is in microseconds. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that payload is a valid pointer to payload_size bytes of data. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_media_frame( media: u32, payload: *const u8, payload_size: usize, timestamp_us: u64, ) -> i32 { ffi::enter(move || { let media = ffi::parse_id(media)?; let payload = unsafe { ffi::parse_slice(payload, payload_size)? }; let timestamp = hang::container::Timestamp::from_micros(timestamp_us)?; State::lock().publish.media_frame(media, payload, timestamp) }) } /// Mark a timeline break without lowering the advertised jitter or delay. /// /// Publishes a discontinuity marker; resumed frames must continue the broadcast media clock, /// and video must resume on a keyframe. /// Returns zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_media_discontinuity(media: u32) -> i32 { ffi::enter(move || { let media = ffi::parse_id(media)?; State::lock().publish.media_discontinuity(media) }) } /// Replace the catalog properties shared by every video rendition. /// /// 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. /// /// Returns zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that `properties` points to a valid [moq_video_properties]. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_video_properties(broadcast: u32, properties: *const moq_video_properties) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let properties = unsafe { properties.as_ref() }.ok_or(Error::InvalidPointer)?; let mut value = hang::catalog::VideoProperties::default(); value.display = properties.has_display.then_some(hang::catalog::Display { width: properties.display_width, height: properties.display_height, }); value.rotation = properties.has_rotation.then_some(properties.rotation); value.flip = properties.has_flip.then_some(properties.flip); State::lock().publish.video_properties(broadcast, value) }) } /// Add or replace a video rendition in a broadcast's catalog. /// /// This is the producer counterpart to [moq_consume_video_config]: instead of /// reading a rendition out of a catalog, it writes one into the catalog of a /// broadcast created with [moq_origin_create_broadcast]. The rendition is keyed by /// `config.name`; calling this again with the same name replaces the rendition /// you declared, so a config can be refined in place. It fails only when a /// [moq_publish_video] track owns the name, since that track publishes and /// retires its own rendition. The updated catalog is published to subscribers /// automatically. /// /// The struct fields are read as inputs: /// - `name` / `codec` are required (NOT NULL terminated) string slices. /// - `label` may be NULL to omit the human-readable rendition name. /// - `description` may be NULL to omit it. /// - `coded_width` / `coded_height` may be zero to omit them. /// - `container` describes how the frames written to the track are wrapped. A /// zeroed one declares the legacy container, which is what [moq_publish_video] /// writes; declare CMAF or LOC for a [moq_publish_track] whose frames you /// already encode that way. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that `config` points to a valid [moq_video_config]. /// - The caller must ensure each non-NULL pointer inside `config` is valid for its length. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_video_config(broadcast: u32, config: *const moq_video_config) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?; let name = unsafe { ffi::parse_str(config.name, config.name_len)? }; let label = unsafe { ffi::parse_str_optional(config.label, config.label_len)? }; let codec = unsafe { ffi::parse_str(config.codec, config.codec_len)? }; let codec = hang::catalog::VideoCodec::from_str(codec).map_err(Error::Hang)?; let mut video = hang::catalog::VideoConfig::new(codec); video.label = label.map(str::to_string); video.preference = config.preference; if !config.description.is_null() { let description = unsafe { ffi::parse_slice(config.description, config.description_len)? }; video.description = Some(bytes::Bytes::copy_from_slice(description)); } video.coded_width = (config.coded_width > 0).then_some(config.coded_width); video.coded_height = (config.coded_height > 0).then_some(config.coded_height); video.container = unsafe { parse_container(&config.container)? }; State::lock().publish.video_config(broadcast, name, video) }) } /// Add or replace an audio rendition in a broadcast's catalog. /// /// This is the producer counterpart to [moq_consume_audio_config]. The rendition /// is keyed by `config.name`, on the same terms as [moq_publish_video_config]: /// a repeat call replaces your own rendition, and a name a [moq_publish_audio] /// track owns is refused. The updated catalog is published to subscribers /// automatically. /// /// The struct fields are read as inputs: /// - `name` / `codec` are required (NOT NULL terminated) string slices. /// - `label` may be NULL to omit the human-readable rendition name. /// - `sample_rate` / `channel_count` are required. /// - `description` may be NULL to omit it. /// - `container` describes how the frames written to the track are wrapped, the /// same as for [moq_publish_video_config]. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that `config` points to a valid [moq_audio_config]. /// - The caller must ensure each non-NULL pointer inside `config` is valid for its length. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_audio_config(broadcast: u32, config: *const moq_audio_config) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?; let name = unsafe { ffi::parse_str(config.name, config.name_len)? }; let label = unsafe { ffi::parse_str_optional(config.label, config.label_len)? }; let codec = unsafe { ffi::parse_str(config.codec, config.codec_len)? }; let codec = hang::catalog::AudioCodec::from_str(codec).map_err(Error::Hang)?; let mut audio = hang::catalog::AudioConfig::new(codec, config.sample_rate, config.channel_count); audio.label = label.map(str::to_string); audio.container = unsafe { parse_container(&config.container)? }; if !config.description.is_null() { let description = unsafe { ffi::parse_slice(config.description, config.description_len)? }; audio.description = Some(bytes::Bytes::copy_from_slice(description)); } State::lock().publish.audio_config(broadcast, name, audio) }) } /// Remove a video rendition from a broadcast's catalog by name. /// /// Removes a rendition added by [moq_publish_video_config]. Any other name is a /// no-op, including one a [moq_publish_video] track owns, which is retired by /// [moq_publish_media_finish] instead. The updated catalog is published to /// subscribers automatically. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that name is a valid pointer to name_len bytes of data. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_video_remove(broadcast: u32, name: *const c_char, name_len: usize) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let name = unsafe { ffi::parse_str(name, name_len)? }; State::lock().publish.video_remove(broadcast, name) }) } /// Remove an audio rendition from a broadcast's catalog by name. /// /// Same rules as [moq_publish_video_remove]. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that name is a valid pointer to name_len bytes of data. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_audio_remove(broadcast: u32, name: *const c_char, name_len: usize) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let name = unsafe { ffi::parse_str(name, name_len)? }; State::lock().publish.audio_remove(broadcast, name) }) } /// Set (or replace) a top-level application catalog section by name. /// /// This is the producer counterpart to [moq_consume_catalog_section] / /// [moq_consume_catalog_section_at]: it writes an arbitrary top-level JSON key into the /// catalog of a broadcast created with [moq_origin_create_broadcast], beyond the /// `video`/`audio` keys owned by the media pipeline. Calling it again with the /// same name replaces the section. The updated catalog is published to /// subscribers automatically. /// /// `json` is a JSON document (object, array, string, ...) as `json_len` bytes of /// UTF-8. Returns a zero on success, or a negative code on failure: invalid JSON /// yields a Json error (-37); a reserved `name` (`video`/`audio`) yields a mux error. /// /// # Safety /// - The caller must ensure that name is a valid pointer to name_len bytes of data. /// - The caller must ensure that json is a valid pointer to json_len bytes of data. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_catalog_section( broadcast: u32, name: *const c_char, name_len: usize, json: *const c_char, json_len: usize, ) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let name = unsafe { ffi::parse_str(name, name_len)? }; let json = unsafe { ffi::parse_str(json, json_len)? }; let value: serde_json::Value = serde_json::from_str(json)?; State::lock().publish.catalog_section_set(broadcast, name, value) }) } /// Remove a top-level application catalog section by name. /// /// This is a no-op if no section with that name exists. The updated catalog is /// published to subscribers automatically. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that name is a valid pointer to name_len bytes of data. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_catalog_section_remove( broadcast: u32, name: *const c_char, name_len: usize, ) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let name = unsafe { ffi::parse_str(name, name_len)? }; State::lock().publish.catalog_section_remove(broadcast, name) }) } /// Create a raw track on a broadcast for arbitrary byte payloads. /// /// Unlike [moq_publish_audio] and [moq_publish_video], this is the bare moq-net primitive: no /// codec, container, or catalog framing. Frames written to it are delivered /// as-is to subscribers using [moq_consume_track]. Use it for non-media tracks /// (control channels, JSON metadata, etc.), or pair it with /// [moq_publish_video_config] / [moq_publish_audio_config] to also describe the /// track in the catalog. Pass NULL for `info` to use moq-net defaults. /// /// Returns a non-zero handle to the track on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that name is a valid pointer to name_len bytes of data. /// - The caller must ensure that info is either NULL or a valid pointer to a [moq_track_info] struct. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_track( broadcast: u32, name: *const c_char, name_len: usize, info: *const moq_track_info, ) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let name = unsafe { ffi::parse_str(name, name_len)? }; let info = unsafe { parse_track_info(info)? }; State::lock().publish.track(broadcast, name, Some(info)) }) } /// Raw track info from an optional C struct, defaulting to a microsecond timescale. /// /// # Safety /// - `info` must be NULL or a valid pointer to a [moq_track_info] struct. unsafe fn parse_track_info(info: *const moq_track_info) -> Result { // Default raw tracks to a microsecond timescale even when no info is given. match unsafe { info.as_ref() } { Some(info) => moq_net::track::Info::try_from(info), None => Ok(moq_net::track::Info::default().with_timescale(moq_net::Timescale::MICRO)), } } /// Append a new group to a raw track, returning a group producer. /// /// Groups are delivered independently and each may contain any number of frames /// written via [moq_publish_group_frame]. Sequence numbers auto-increment. /// /// Returns a non-zero handle to the group on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_track_group(track: u32) -> i32 { ffi::enter(move || { let track = ffi::parse_id(track)?; State::lock().publish.track_group(track) }) } /// Create a raw group with an explicit sequence number. /// /// Returns a non-zero group handle on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_track_group_at(track: u32, sequence: u64) -> i32 { ffi::enter(move || { let track = ffi::parse_id(track)?; State::lock().publish.track_group_at(track, sequence) }) } /// Write a single-frame group to a raw track with a timestamp. /// /// Convenience for the common one-frame-per-group pattern. Equivalent to /// appending a group, writing one frame, and finishing it. /// The timestamp is in microseconds. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that payload is a valid pointer to payload_size bytes of data. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_track_frame( track: u32, payload: *const u8, payload_size: usize, timestamp_us: u64, ) -> i32 { ffi::enter(move || { let track = ffi::parse_id(track)?; let payload = unsafe { ffi::parse_slice(payload, payload_size)? }; let timestamp = moq_net::Timestamp::from_micros(timestamp_us)?; State::lock().publish.track_frame(track, timestamp, payload) }) } /// Send a best-effort datagram on a raw track created by [moq_publish_track]. /// /// Takes `payload` then `timestamp_us`, matching [moq_publish_track_frame]. The payload must /// be at most 1200 bytes. On success the datagram's per-track sequence number (shared with the /// group namespace) is written to `out_sequence` when it is non-NULL. Datagrams are /// delivered only on transports and wire versions with a datagram channel; there is no /// group fallback. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that payload is a valid pointer to payload_size bytes of data. /// - `out_sequence` must be NULL or a valid pointer to a `uint64_t`. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_track_datagram( track: u32, payload: *const u8, payload_size: usize, timestamp_us: u64, out_sequence: *mut u64, ) -> i32 { ffi::enter(move || { let track = ffi::parse_id(track)?; let payload = unsafe { ffi::parse_slice(payload, payload_size)? }; let sequence = State::lock().publish.track_datagram(track, timestamp_us, payload)?; if let Some(out) = unsafe { out_sequence.as_mut() } { *out = sequence; } Ok(()) }) } /// Finish a raw track and release its handle. No more groups or frames can be written. /// /// The broadcast keeps the finished track until it closes, so a late subscriber can still /// drain it. /// /// Returns a zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_track_finish(track: u32) -> i32 { ffi::enter(move || { let track = ffi::parse_id(track)?; State::lock().publish.track_finish(track) }) } /// Declare a raw track's exclusive final group sequence. /// /// Groups below `final_sequence` may still be created. Groups at or above it /// are rejected. The track remains open for groups below the boundary. Call /// [moq_publish_track_finish] after producing the remaining groups. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_track_finish_at(track: u32, final_sequence: u64) -> i32 { ffi::enter(move || { let track = ffi::parse_id(track)?; State::lock().publish.track_finish_at(track, final_sequence) }) } /// Abort a raw track with an application error code. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_track_abort(track: u32, error_code: u16) -> i32 { ffi::enter(move || { let track = ffi::parse_id(track)?; State::lock().publish.track_abort(track, error_code) }) } /// Watch whether a raw track has subscribers. See [moq_publish_media_demand] for the /// callback contract. /// /// Returns a non-zero watcher handle on success, or a negative code on failure. /// /// # Safety /// - `on_demand` must be non-NULL. /// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_demand` callback. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_track_demand( track: u32, on_demand: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let track = ffi::parse_id(track)?; let on_demand = unsafe { ffi::OnStatus::new(user_data, on_demand)? }; let mut state = State::lock(); let demand = state.publish.track_demand(track)?; state.publish.demand(demand, on_demand) }) } /// Serve subscriber requests for tracks the broadcast has not declared. /// /// Without a live handler a subscription to an unknown track name is refused. While one /// is live, `on_request` is invoked with a positive request handle for each pending /// track, then exactly once more with a terminal code: `0` (the broadcast finished, or /// [moq_publish_requests_cancel] was called) or a negative error. After the terminal /// (`<= 0`) callback, `user_data` is never touched again. Answer each request with /// [moq_track_request_accept], [moq_track_request_video], [moq_track_request_audio], /// or [moq_track_request_abort]; the subscriber waits until you do. /// /// Returns a non-zero handle on success, or a negative code on failure. /// /// # Safety /// - `on_request` must be non-NULL. /// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_request` callback. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_requests( broadcast: u32, on_request: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let on_request = unsafe { ffi::OnStatus::new(user_data, on_request)? }; State::lock().publish.dynamic(broadcast, on_request) }) } /// Serve fetches of groups a raw track no longer has cached. /// /// Without a live handler a fetch that misses the cache fails as not found. While one is /// live, `on_group` is invoked with a positive group-request handle for each miss, then /// exactly once more with a terminal code: `0` (the track ended, or /// [moq_publish_requests_cancel] was called) or a negative error. After the terminal /// (`<= 0`) callback, `user_data` is never touched again. Cached groups never reach the /// handler. Answer each request with [moq_group_request_accept] or [moq_group_request_abort]. /// A request whose group nobody reads any more is withdrawn: answering it then fails as not /// found, so move on to the next request. /// /// Returns a non-zero handle on success, or a negative code on failure. /// /// # Safety /// - `on_group` must be non-NULL. /// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_group` callback. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_track_requests( track: u32, on_group: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let track = ffi::parse_id(track)?; let on_group = unsafe { ffi::OnStatus::new(user_data, on_group)? }; State::lock().publish.track_dynamic(track, on_group) }) } /// Stop a request handler from [moq_publish_requests], [moq_publish_track_requests], or /// [moq_track_request_requests]. Requests not yet delivered are rejected. /// /// Returns immediately: zero on success, or a negative code if already closed. The /// handler's callback still fires once more with a terminal `0`, and that final /// callback is where `user_data` should be released. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_requests_cancel(dynamic: u32) -> i32 { ffi::enter(move || { let dynamic = ffi::parse_id(dynamic)?; State::lock().publish.dynamic_close(dynamic) }) } /// The name of a track request delivered to a [moq_publish_requests] callback. /// /// The destination borrows the request's storage: copy it out before accepting, /// aborting, or freeing the request. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - `dst` must point at a writable [moq_string]. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_track_request_name(request: u32, dst: *mut moq_string) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; State::lock().publish.track_request_name(request, dst) }) } /// Serve fetches of uncached groups on a requested track, before accepting it. /// /// A track requested by a fetch has that group pending from birth. Register the /// handler here, before [moq_track_request_accept], so the request survives the /// transition; the callback contract is that of [moq_publish_track_requests]. /// /// Returns a non-zero handle on success, or a negative code on failure. /// /// # Safety /// - `on_group` must be non-NULL. /// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_group` callback. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_track_request_requests( request: u32, on_group: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; let on_group = unsafe { ffi::OnStatus::new(user_data, on_group)? }; State::lock().publish.track_request_dynamic(request, on_group) }) } /// Accept a track request as a raw track, resolving the waiting subscribers. /// /// Consumes the request handle. `info` is as in [moq_publish_track]: NULL for the /// microsecond default. Returns a non-zero track handle usable with every /// `moq_publish_track_*` function, or a negative code on failure. Once the request's /// broadcast closed, the track ends with `UNROUTABLE` and the call fails with /// `MOQ_ERROR_BROADCAST_NOT_FOUND`. /// /// # Safety /// - `info` must be NULL or a valid pointer to a [moq_track_info] struct. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_track_request_accept(request: u32, info: *const moq_track_info) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; let info = unsafe { parse_track_info(info)? }; State::lock().publish.track_request_accept(request, info) }) } /// Accept a track request as an audio track, the importer picking the timescale. /// /// Consumes the request handle. Returns the same kind of media handle as /// [moq_publish_audio], or a negative code on failure. /// /// # Safety /// - As [moq_publish_audio], for `config`. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_track_request_audio(request: u32, config: *const moq_audio_init) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; let (audio, hls) = unsafe { parse_audio_init(config)? }; State::lock().publish.track_request_audio(request, audio, hls) }) } /// Accept a track request as a video track, the importer picking the timescale. /// /// Consumes the request handle. Returns the same kind of media handle as /// [moq_publish_video], or a negative code on failure. /// /// # Safety /// - As [moq_publish_audio], for a [moq_video_init]. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_track_request_video(request: u32, config: *const moq_video_init) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; let (video, hls) = unsafe { parse_video_init(config)? }; State::lock().publish.track_request_video(request, video, hls) }) } /// Reject a track request with an application error code, failing the waiting subscribers. /// /// Consumes the request handle. Returns a zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_track_request_abort(request: u32, error_code: u16) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; State::lock().publish.track_request_abort(request, error_code) }) } /// Free a track request without accepting it, which rejects it. /// /// Returns a zero on success, or a negative code if the handle is unknown. #[unsafe(no_mangle)] pub extern "C" fn moq_track_request_free(request: u32) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; State::lock().publish.track_request_free(request) }) } /// The group sequence a group request asks for. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - `dst` must point at a writable `uint64_t`. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_group_request_sequence(request: u32, dst: *mut u64) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; *dst = State::lock().publish.group_request_info(request)?.0; Ok(()) }) } /// The delivery priority the fetching consumer asked for. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - `dst` must point at a writable `int8_t`. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_group_request_priority(request: u32, dst: *mut i8) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; *dst = State::lock().publish.group_request_info(request)?.1; Ok(()) }) } /// The first frame of the group the fetch wants; 0 is the whole group. /// /// [moq_group_request_accept] positions the returned producer here, so frames you /// write keep the indices they have in the group rather than restarting at 0. Read /// this to know which frames to fetch from storage. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - `dst` must point at a writable `uint64_t`. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_group_request_frame_start(request: u32, dst: *mut u64) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; *dst = State::lock().publish.group_request_info(request)?.2; Ok(()) }) } /// Accept a group request, resolving the waiting fetches with the group you then fill. /// /// Consumes the request handle. The returned producer starts at /// [moq_group_request_frame_start], so the first frame you write lands at that /// index. Returns a non-zero group handle usable with [moq_publish_group_frame] /// and [moq_publish_group_finish], or a negative code on failure, including when /// the group is already cached. #[unsafe(no_mangle)] pub extern "C" fn moq_group_request_accept(request: u32) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; State::lock().publish.group_request_accept(request) }) } /// Reject a group request, failing the readers waiting on it now as not found. /// /// A later reader of the group asks again. The error code is not delivered. Consumes the /// request handle. Returns a zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_group_request_abort(request: u32, error_code: u16) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; State::lock().publish.group_request_abort(request, error_code) }) } /// Free a group request without accepting it, which rejects it like [moq_group_request_abort]. /// /// Returns a zero on success, or a negative code if the handle is unknown. #[unsafe(no_mangle)] pub extern "C" fn moq_group_request_free(request: u32) -> i32 { ffi::enter(move || { let request = ffi::parse_id(request)?; State::lock().publish.group_request_free(request) }) } /// Write a frame into a raw group created by [moq_publish_track_group]. /// /// The timestamp is in microseconds. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that payload is a valid pointer to payload_size bytes of data. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_group_frame( group: u32, payload: *const u8, payload_size: usize, timestamp_us: u64, ) -> i32 { ffi::enter(move || { let group = ffi::parse_id(group)?; let payload = unsafe { ffi::parse_slice(payload, payload_size)? }; let timestamp = moq_net::Timestamp::from_micros(timestamp_us)?; State::lock().publish.group_frame(group, timestamp, payload) }) } /// Finish a raw group. No more frames can be written. /// /// Returns a zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_group_finish(group: u32) -> i32 { ffi::enter(move || { let group = ffi::parse_id(group)?; State::lock().publish.group_finish(group) }) } /// Abort a raw group with an application error code. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_group_abort(group: u32, error_code: u16) -> i32 { ffi::enter(move || { let group = ffi::parse_id(group)?; State::lock().publish.group_abort(group, error_code) }) } /// Create a JSON snapshot track (lossy latest-value) on a broadcast. /// /// Values published via [moq_publish_json_snapshot_update] reach subscribers as a single latest /// state; a late joiner only sees the newest. The track is advertised in the broadcast's catalog /// under `json.tracks.` with `mode: snapshot` (and `compression: deflate` when set), and the /// entry is retired when the track finishes or fails, so consumers discover it with no extra call. /// /// Returns a non-zero handle to the JSON producer on success, or a negative code on failure, /// including a mux error when the catalog already carries an entry named `name`. /// /// # Safety /// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_json_snapshot( broadcast: u32, name: *const c_char, name_len: usize, config: *const moq_json_snapshot_config, ) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let name = unsafe { ffi::parse_str(name, name_len)? }; let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?; let entry = moq_mux::json::Config::default() .with_compression(config.compression) .with_bitrate(supplied_bitrate(config.has_bitrate, config.bitrate)?); let options = moq_mux::json::Options::default().with_deltas(config.deltas); State::lock().publish.json_snapshot(broadcast, name, entry, options) }) } /// Publish a new value to a JSON snapshot track. `value` is a UTF-8 JSON document. A no-op if /// unchanged from the previous update. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure `value` is a valid pointer to `value_len` bytes. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_json_snapshot_update(json: u32, value: *const c_char, value_len: usize) -> i32 { ffi::enter(move || { let json = ffi::parse_id(json)?; let value = unsafe { ffi::parse_slice(value.cast::(), value_len)? }; let value = serde_json::from_slice(value)?; State::lock().publish.json_snapshot_update(json, value) }) } /// Finish a JSON snapshot track. No more values can be published. /// /// Returns a zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_json_snapshot_finish(json: u32) -> i32 { ffi::enter(move || { let json = ffi::parse_id(json)?; State::lock().publish.json_snapshot_finish(json) }) } /// Create a JSON stream track (lossless append-log) on a broadcast. /// /// Every record appended via [moq_publish_json_stream_append] is preserved and delivered in order. /// The track is advertised in the broadcast's catalog under `json.tracks.` with /// `mode: stream`, for as long as the track lives. /// /// Returns a non-zero handle to the JSON stream producer on success, or a negative code on failure, /// including a mux error when the catalog already carries an entry named `name`. /// /// # Safety /// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_json_stream( broadcast: u32, name: *const c_char, name_len: usize, config: *const moq_json_stream_config, ) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let name = unsafe { ffi::parse_str(name, name_len)? }; let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?; let producer = moq_mux::json::Config::default() .with_compression(config.compression) .with_bitrate(supplied_bitrate(config.has_bitrate, config.bitrate)?); State::lock().publish.json_stream(broadcast, name, producer) }) } /// Append one record to a JSON stream track. `value` is a UTF-8 JSON document. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure `value` is a valid pointer to `value_len` bytes. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_json_stream_append(stream: u32, value: *const c_char, value_len: usize) -> i32 { ffi::enter(move || { let stream = ffi::parse_id(stream)?; let value = unsafe { ffi::parse_slice(value.cast::(), value_len)? }; let value = serde_json::from_slice(value)?; State::lock().publish.json_stream_append(stream, value) }) } /// Finish a JSON stream track. No more records can be appended. /// /// Returns a zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_json_stream_finish(stream: u32) -> i32 { ffi::enter(move || { let stream = ffi::parse_id(stream)?; State::lock().publish.json_stream_finish(stream) }) } /// Parse a [moq_binary_config] into the mux's binary track config. /// /// # Safety /// - `config` must be a valid pointer, and its `mime` a valid pointer to `mime_len` bytes when not NULL. unsafe fn binary_config(config: *const moq_binary_config) -> Result { let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?; let mut binary = moq_mux::binary::Config::default() .with_compression(config.compression) .with_bitrate(supplied_bitrate(config.has_bitrate, config.bitrate)?); if let Some(mime) = unsafe { ffi::parse_str_optional(config.mime, config.mime_len)? } { binary = binary.with_mime(mime); } Ok(binary) } /// Create a binary snapshot track (lossy latest-value) on a broadcast: each payload supersedes the /// last, and a late joiner only sees the newest, e.g. the latest thumbnail of a camera. /// /// The track is advertised in the broadcast's catalog under `binary.tracks.` with /// `mode: snapshot` (plus `mime` and `compression` when set), and the entry is retired when the /// track finishes or fails. /// /// Returns a non-zero handle to the binary producer on success, or a negative code on failure, /// including a mux error when the catalog already carries an entry named `name`. /// /// # Safety /// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_binary_snapshot( broadcast: u32, name: *const c_char, name_len: usize, config: *const moq_binary_config, ) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let name = unsafe { ffi::parse_str(name, name_len)? }; let config = unsafe { binary_config(config)? }; State::lock().publish.binary_snapshot(broadcast, name, config) }) } /// Publish a new payload to a binary snapshot track, superseding the last. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure `payload` is a valid pointer to `payload_len` bytes. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_binary_snapshot_update( binary: u32, payload: *const u8, payload_len: usize, ) -> i32 { ffi::enter(move || { let binary = ffi::parse_id(binary)?; let payload = unsafe { ffi::parse_slice(payload, payload_len)? }; State::lock().publish.binary_snapshot_update(binary, payload) }) } /// Finish a binary snapshot track and retire its catalog entry. No more payloads can be published. /// /// Returns a zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_binary_snapshot_finish(binary: u32) -> i32 { ffi::enter(move || { let binary = ffi::parse_id(binary)?; State::lock().publish.binary_snapshot_finish(binary) }) } /// Create a binary stream track (lossless append-log) on a broadcast: every payload is preserved /// and delivered in order. /// /// The track is advertised in the broadcast's catalog under `binary.tracks.` with /// `mode: stream` (plus `mime` and `compression` when set), for as long as the track lives. /// /// Returns a non-zero handle to the binary stream producer on success, or a negative code on /// failure, including a mux error when the catalog already carries an entry named `name`. /// /// # Safety /// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_binary_stream( broadcast: u32, name: *const c_char, name_len: usize, config: *const moq_binary_config, ) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let name = unsafe { ffi::parse_str(name, name_len)? }; let config = unsafe { binary_config(config)? }; State::lock().publish.binary_stream(broadcast, name, config) }) } /// Append one payload to a binary stream track. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure `payload` is a valid pointer to `payload_len` bytes. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_publish_binary_stream_append(stream: u32, payload: *const u8, payload_len: usize) -> i32 { ffi::enter(move || { let stream = ffi::parse_id(stream)?; let payload = unsafe { ffi::parse_slice(payload, payload_len)? }; State::lock().publish.binary_stream_append(stream, payload) }) } /// Finish a binary stream track and retire its catalog entry. No more payloads can be appended. /// /// Returns a zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_publish_binary_stream_finish(stream: u32) -> i32 { ffi::enter(move || { let stream = ffi::parse_id(stream)?; State::lock().publish.binary_stream_finish(stream) }) } /// Create a catalog consumer for a broadcast. /// /// `on_catalog` is invoked with a positive catalog ID for each catalog update /// (usable to query video/audio track information), then exactly once more with /// a terminal code: `0` (closed cleanly) or a negative error. After the terminal /// (`<= 0`) callback, `on_catalog` is never called again and `user_data` is never /// touched again, so release `user_data` there. The terminal callback fires even /// after [moq_consume_catalog_cancel]. /// /// Returns a non-zero handle on success, or a negative code on failure. /// /// # Safety /// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_catalog` callback. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_consume_catalog( broadcast: u32, on_catalog: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let on_catalog = unsafe { ffi::OnStatus::new(user_data, on_catalog)? }; State::lock().consume.catalog(broadcast, on_catalog) }) } /// Stop a catalog consumer's background subscription. /// /// Returns immediately: zero on success, or a negative code if already closed. /// Does NOT free `user_data`; the [moq_consume_catalog] callback still fires once /// more with a terminal `0` (or a negative error), which is where `user_data` /// should be released. Catalog snapshots previously delivered via the callback /// remain valid until freed with [moq_consume_catalog_free]. #[unsafe(no_mangle)] pub extern "C" fn moq_consume_catalog_cancel(catalog: u32) -> i32 { ffi::enter(move || { let catalog = ffi::parse_id(catalog)?; State::lock().consume.catalog_close(catalog) }) } /// Free a catalog snapshot received via the [moq_consume_catalog] callback. /// /// This releases the snapshot and invalidates any borrowed references (e.g. pointers /// returned by [moq_consume_video_config] or [moq_consume_audio_config]). /// /// Returns a zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_consume_catalog_free(catalog: u32) -> i32 { ffi::enter(move || { let catalog = ffi::parse_id(catalog)?; State::lock().consume.catalog_free(catalog) }) } /// Query information about a video track in a catalog. /// /// The destination is filled with the video track information. `dst->container` /// says how the track's frames are wrapped; skip a rendition whose kind is /// `MOQ_CONTAINER_KIND_UNKNOWN`, since this build cannot parse it. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that `dst` is a valid pointer to a [moq_video_config] struct. /// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_consume_video_config(catalog: u32, index: u32, dst: *mut moq_video_config) -> i32 { ffi::enter(move || { let catalog = ffi::parse_id(catalog)?; let index = index as usize; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; State::lock().consume.video_config(catalog, index, dst) }) } /// Query whether a video rendition may be selected. /// /// False means no frames are coming and the rendition must not be selected. A catalog that /// omits the optional field reads as true. /// /// Returns zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that `dst` points to properly aligned, writable storage for a `bool`. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_consume_video_enabled(catalog: u32, index: u32, dst: *mut bool) -> i32 { ffi::enter(move || { let catalog = ffi::parse_id(catalog)?; if dst.is_null() { return Err(Error::InvalidPointer); } let enabled = State::lock().consume.video_enabled(catalog, index as usize)?; unsafe { dst.write(enabled) }; Ok(()) }) } /// Query the catalog properties shared by every video rendition. /// /// The destination is filled by value and remains valid after the catalog snapshot is freed. /// Inspect each `has_*` flag before reading its value. /// /// Returns zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that `dst` points to a valid [moq_video_properties]. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_consume_video_properties(catalog: u32, dst: *mut moq_video_properties) -> i32 { ffi::enter(move || { let catalog = ffi::parse_id(catalog)?; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; State::lock().consume.video_properties(catalog, dst) }) } /// Query information about an audio track in a catalog. /// /// The destination is filled with the audio track information. `dst->container` /// says how the track's frames are wrapped; skip a rendition whose kind is /// `MOQ_CONTAINER_KIND_UNKNOWN`, since this build cannot parse it. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that `dst` is a valid pointer to a [moq_audio_config] struct. /// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_consume_audio_config(catalog: u32, index: u32, dst: *mut moq_audio_config) -> i32 { ffi::enter(move || { let catalog = ffi::parse_id(catalog)?; let index = index as usize; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; State::lock().consume.audio_config(catalog, index, dst) }) } /// Number of untyped application catalog sections in a catalog snapshot. /// /// These are the top-level catalog keys beyond `video`/`audio`, carried through /// verbatim. Iterate them by index with [moq_consume_catalog_section_at], or look one up /// directly by name with [moq_consume_catalog_section]. /// /// Returns the count (>= 0) on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_consume_catalog_section_count(catalog: u32) -> i32 { ffi::enter(move || { let catalog = ffi::parse_id(catalog)?; State::lock().consume.catalog_section_count(catalog) }) } /// Query an application catalog section by index, keyed by name. /// /// Fills `dst` with the section's name and JSON value at `index`, in the range /// `[0, moq_consume_catalog_section_count)`. Both pointers borrow the snapshot's storage /// and stay valid until it is freed with [moq_consume_catalog_free]. /// /// Returns a zero on success, or a negative code on failure (e.g. `index` out of /// range). /// /// # Safety /// - The caller must ensure that `dst` is a valid pointer to a [moq_section] struct. /// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_consume_catalog_section_at(catalog: u32, index: u32, dst: *mut moq_section) -> i32 { ffi::enter(move || { let catalog = ffi::parse_id(catalog)?; let index = index as usize; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; State::lock().consume.catalog_section_at(catalog, index, dst) }) } /// Look up an application catalog section by name. /// /// Fills `dst` with the section's JSON value (the document to parse yourself). /// The pointer borrows the snapshot's storage and stays valid until it is freed /// with [moq_consume_catalog_free]. /// /// Returns a zero on success, or a negative code on failure: no section with that /// name yields a not-found error. /// /// # Safety /// - The caller must ensure that name is a valid pointer to name_len bytes of data. /// - The caller must ensure that `dst` is a valid pointer to a [moq_string] struct. /// - The caller must ensure that `dst` is not used after [moq_consume_catalog_free] is called. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_consume_catalog_section( catalog: u32, name: *const c_char, name_len: usize, dst: *mut moq_string, ) -> i32 { ffi::enter(move || { let catalog = ffi::parse_id(catalog)?; let name = unsafe { ffi::parse_str(name, name_len)? }; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; State::lock().consume.catalog_section_get(catalog, name, dst) }) } /// Consume a video track from a broadcast, delivering encoded frames, gaps, and encoder ends in order. /// /// - `max_delay_us` controls the maximum amount of buffering allowed before skipping a GoP. /// - `on_read` is called with a positive read ID per item, then exactly once /// more with a terminal code: `0` (closed cleanly) or a negative error. After /// the terminal (`<= 0`) callback, `on_read` is never called again and /// `user_data` is never touched again, so release `user_data` there. The /// terminal callback fires even after [moq_consume_video_cancel]. /// /// Returns a non-zero handle to the track on success, or a negative code on failure. /// /// # Safety /// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_read` callback. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_consume_video( catalog: u32, index: u32, max_delay_us: u64, on_read: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let catalog = ffi::parse_id(catalog)?; let index = index as usize; let max_delay = std::time::Duration::from_micros(max_delay_us); let on_read = unsafe { ffi::OnStatus::new(user_data, on_read)? }; State::lock() .consume .media::(catalog, index, max_delay, on_read) }) } /// Stop a video track consumer's background task. /// /// Returns immediately: zero on success, or a negative code if already closed. /// Does NOT free `user_data`; the [moq_consume_video] `on_read` callback /// still fires once more with a terminal `0` (or a negative error), which is /// where `user_data` should be released. #[unsafe(no_mangle)] pub extern "C" fn moq_consume_video_cancel(track: u32) -> i32 { ffi::enter(move || { let track = ffi::parse_id(track)?; State::lock().consume.track_close(track) }) } /// Consume an audio track from a broadcast, delivering encoded frames, gaps, and encoder ends in order. /// /// `on_read` is called with a positive read ID per item, then exactly once /// more with a terminal code: `0` (closed cleanly) or a negative error. After /// the terminal (`<= 0`) callback, `on_read` is never called again and /// `user_data` is never touched again, so release `user_data` there. The /// terminal callback fires even after [moq_consume_audio_cancel]. /// The `max_delay_us` parameter controls how long to wait before skipping frames. /// /// Returns a non-zero handle to the track on success, or a negative code on failure. /// /// # Safety /// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_read` callback. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_consume_audio( catalog: u32, index: u32, max_delay_us: u64, on_read: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let catalog = ffi::parse_id(catalog)?; let index = index as usize; let max_delay = std::time::Duration::from_micros(max_delay_us); let on_read = unsafe { ffi::OnStatus::new(user_data, on_read)? }; State::lock() .consume .media::(catalog, index, max_delay, on_read) }) } /// Stop an audio track consumer's background task. /// /// Returns immediately: zero on success, or a negative code if already closed. /// Does NOT free `user_data`; the [moq_consume_audio] `on_read` callback /// still fires once more with a terminal `0` (or a negative error), which is /// where `user_data` should be released. #[unsafe(no_mangle)] pub extern "C" fn moq_consume_audio_cancel(track: u32) -> i32 { ffi::enter(move || { let track = ffi::parse_id(track)?; State::lock().consume.track_close(track) }) } /// Read an encoded media frame, gap, or encoder end delivered by a media callback. /// Payload pointers borrow the read until [`moq_consume_read_free`]. /// /// # Safety /// - `dst` must point to a writable [`moq_media_read`]. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_consume_read(read: u32, dst: *mut moq_media_read) -> i32 { ffi::enter(move || { let read = ffi::parse_id(read)?; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; State::lock().consume.read(read, dst) }) } /// Free an encoded media read delivered by [`moq_consume_video`] or [`moq_consume_audio`]. #[unsafe(no_mangle)] pub extern "C" fn moq_consume_read_free(read: u32) -> i32 { ffi::enter(move || { let read = ffi::parse_id(read)?; State::lock().consume.read_free(read) }) } /// Close a broadcast consumer and clean up its resources. /// /// Returns a zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_consume_close(consume: u32) -> i32 { ffi::enter(move || { let consume = ffi::parse_id(consume)?; State::lock().consume.close(consume) }) } /// Subscribe to a raw track by name, delivering each frame's payload as-is. /// /// This is the counterpart to [moq_publish_track]: no catalog lookup or /// container parsing. `on_frame` is called with a positive raw frame ID for each /// frame in sequence order, then exactly once more with a terminal code: `0` /// (closed cleanly) or a negative error. After the terminal (`<= 0`) callback, /// `on_frame` is never called again and `user_data` is never touched again, so /// release `user_data` there. The terminal callback fires even after /// [moq_consume_track_cancel]. Read each frame with [moq_consume_track_frame] and /// release it with [moq_consume_track_frame_free]. Pass NULL for `subscription` /// to use moq-net defaults. /// /// Returns a non-zero handle to the track on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that name is a valid pointer to name_len bytes of data. /// - The caller must ensure that subscription is either NULL or a valid pointer to a [moq_subscription] struct, /// whose `ranges` is NULL or points to `ranges_len` ranges. /// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_frame` callback. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_consume_track( broadcast: u32, name: *const c_char, name_len: usize, subscription: *const moq_subscription, on_frame: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let name = unsafe { ffi::parse_str(name, name_len)? }; let subscription = unsafe { subscription.as_ref() } .map(|subscription| unsafe { parse_subscription(subscription) }) .transpose()?; let on_frame = unsafe { ffi::OnStatus::new(user_data, on_frame)? }; State::lock().consume.raw_track(broadcast, name, subscription, on_frame) }) } /// Update a raw track subscription's delivery preferences. /// /// Pass NULL for `subscription` to reset to moq-net defaults. /// /// Returns a zero on success, or a negative code on failure, such as a time anchor on an /// untimed track, which leaves the subscription as it was. An update made before the /// subscription is established applies once it is, so a refusal then ends the consumer /// through its callback instead. /// /// # Safety /// - The caller must ensure that subscription is either NULL or a valid pointer to a [moq_subscription] struct, /// whose `ranges` is NULL or points to `ranges_len` ranges. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_consume_track_update(track: u32, subscription: *const moq_subscription) -> i32 { ffi::enter(move || { let track = ffi::parse_id(track)?; let subscription = unsafe { subscription.as_ref() } .map(|subscription| unsafe { parse_subscription(subscription) }) .transpose()?; State::lock().consume.raw_track_update(track, subscription) }) } /// Read a raw frame's payload delivered via the [moq_consume_track] callback. /// /// Fills `dst.payload` / `dst.payload_size`; the pointer is valid until the /// frame is released with [moq_consume_track_frame_free]. `dst.timestamp_us` is the /// frame presentation timestamp in microseconds, when `dst.timestamp_present` says it /// has one. `dst.keyframe` is reported as /// false because raw tracks do not parse codec metadata. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that `dst` is a valid pointer to a [moq_frame] struct. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_consume_track_frame(frame: u32, dst: *mut moq_frame) -> i32 { ffi::enter(move || { let frame = ffi::parse_id(frame)?; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; State::lock().consume.raw_frame(frame, dst) }) } /// Free a raw frame delivered via the [moq_consume_track] callback, releasing its payload. /// /// Returns a zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_consume_track_frame_free(frame: u32) -> i32 { ffi::enter(move || { let frame = ffi::parse_id(frame)?; State::lock().consume.raw_frame_close(frame) }) } /// Stop a raw track consumer's background task. /// /// Returns immediately: zero on success, or a negative code if already closed. /// Does NOT free `user_data`; the [moq_consume_track] `on_frame` callback still /// fires once more with a terminal `0` (or a negative error), which is where /// `user_data` should be released. Frames already delivered via the callback /// remain valid until released with [moq_consume_track_frame_free]. #[unsafe(no_mangle)] pub extern "C" fn moq_consume_track_cancel(track: u32) -> i32 { ffi::enter(move || { let track = ffi::parse_id(track)?; State::lock().consume.raw_track_close(track) }) } /// Subscribe to a raw track's best-effort datagrams by name. /// /// The datagram counterpart to [moq_consume_track], on its own subscription. `on_datagram` /// is called with a positive datagram ID for each datagram in arrival order, then exactly /// once more with a terminal code: `0` (closed cleanly) or a negative error. After the /// terminal (`<= 0`) callback, `on_datagram` is never called again and `user_data` is never /// touched again, so release `user_data` there. The terminal callback fires even after /// [moq_consume_datagrams_cancel]. Read each datagram with [moq_consume_datagram] and release /// it with [moq_consume_datagram_free]. Datagrams arrive only over datagram-capable /// transports on moq-transport or lite-05 and newer moq-lite; there is no stream fallback. /// /// Returns a non-zero handle to the subscription on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that name is a valid pointer to name_len bytes of data. /// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_datagram` callback. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_consume_datagrams( broadcast: u32, name: *const c_char, name_len: usize, on_datagram: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let name = unsafe { ffi::parse_str(name, name_len)? }; let on_datagram = unsafe { ffi::OnStatus::new(user_data, on_datagram)? }; State::lock().consume.datagram_track(broadcast, name, on_datagram) }) } /// Read a datagram delivered via the [moq_consume_datagrams] callback. /// /// Fills `dst.payload` / `dst.payload_size` (valid until the datagram is released with /// [moq_consume_datagram_free]), plus `dst.timestamp_us` (when `dst.timestamp_present`) /// and `dst.sequence`. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure that `dst` is a valid pointer to a [moq_datagram] struct. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_consume_datagram(datagram: u32, dst: *mut moq_datagram) -> i32 { ffi::enter(move || { let datagram = ffi::parse_id(datagram)?; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; State::lock().consume.datagram(datagram, dst) }) } /// Free a datagram delivered via the [moq_consume_datagrams] callback, releasing its payload. /// /// Returns a zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_consume_datagram_free(datagram: u32) -> i32 { ffi::enter(move || { let datagram = ffi::parse_id(datagram)?; State::lock().consume.datagram_close(datagram) }) } /// Stop a datagram subscription's background task. /// /// Returns immediately: zero on success, or a negative code if already closed. Does NOT free /// `user_data`; the [moq_consume_datagrams] `on_datagram` callback still fires once more with a /// terminal `0` (or a negative error), which is where `user_data` should be released. Datagrams /// already delivered via the callback remain valid until released with [moq_consume_datagram_free]. #[unsafe(no_mangle)] pub extern "C" fn moq_consume_datagrams_cancel(task: u32) -> i32 { ffi::enter(move || { let task = ffi::parse_id(task)?; State::lock().consume.datagram_track_close(task) }) } /// Subscribe to a JSON snapshot track (lossy latest-value) by name. /// /// `on_value` is called with a positive value ID for each state, in order; a group superseded by /// a newer one before it is read is skipped, so a slow reader stays bounded. It is called exactly once more /// with a terminal `0` (track ended / closed) or a negative error, after which `user_data` is /// never touched again, so release it there. Read each value with [moq_consume_json_value] and /// release it with [moq_consume_json_value_free]. Pass the same compression the producer used. /// /// Returns a non-zero handle to the task on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer. /// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_value` callback. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_consume_json_snapshot( broadcast: u32, name: *const c_char, name_len: usize, config: *const moq_json_snapshot_config, on_value: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let name = unsafe { ffi::parse_str(name, name_len)? }; let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?; let mut consumer = moq_json::snapshot::consumer::Config::default(); consumer.compression = if config.compression { moq_json::Compression::Deflate } else { moq_json::Compression::None }; let on_value = unsafe { ffi::OnStatus::new(user_data, on_value)? }; State::lock().consume.json_snapshot(broadcast, name, consumer, on_value) }) } /// Subscribe to a JSON stream track (lossless append-log) by name. /// /// `on_value` is called with a positive value ID for each record, in order, then once more with /// a terminal `0` or negative error where `user_data` should be released. Read each value with /// [moq_consume_json_value] and release it with [moq_consume_json_value_free]. /// /// Returns a non-zero handle to the task on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure `name` is a valid pointer to `name_len` bytes and `config` a valid pointer. /// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_value` callback. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_consume_json_stream( broadcast: u32, name: *const c_char, name_len: usize, config: *const moq_json_stream_config, on_value: ffi::moq_status_callback, user_data: *mut c_void, ) -> i32 { ffi::enter(move || { let broadcast = ffi::parse_id(broadcast)?; let name = unsafe { ffi::parse_str(name, name_len)? }; let config = unsafe { config.as_ref() }.ok_or(Error::InvalidPointer)?; let mut consumer = moq_json::stream::Config::default(); if config.compression { consumer.compression = moq_json::Compression::Deflate; } let on_value = unsafe { ffi::OnStatus::new(user_data, on_value)? }; State::lock().consume.json_stream(broadcast, name, consumer, on_value) }) } /// Read a JSON value delivered via a [moq_consume_json_snapshot] or [moq_consume_json_stream] callback. /// /// Fills `dst.json` / `dst.json_len` and the frame timestamp; the pointer is valid until the value is released with /// [moq_consume_json_value_free]. /// /// Returns a zero on success, or a negative code on failure. /// /// # Safety /// - The caller must ensure `dst` is a valid pointer to a [moq_json_value] struct. #[unsafe(no_mangle)] pub unsafe extern "C" fn moq_consume_json_value(value: u32, dst: *mut moq_json_value) -> i32 { ffi::enter(move || { let value = ffi::parse_id(value)?; let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?; State::lock().consume.json_value(value, dst) }) } /// Release a JSON value delivered via a consumer callback. /// /// Returns a zero on success, or a negative code on failure. #[unsafe(no_mangle)] pub extern "C" fn moq_consume_json_value_free(value: u32) -> i32 { ffi::enter(move || { let value = ffi::parse_id(value)?; State::lock().consume.json_value_close(value) }) } /// Stop a JSON consumer's background task (snapshot or stream). /// /// Returns immediately: zero on success, or a negative code if already closed. Does NOT free /// `user_data`; the `on_value` callback still fires once more with a terminal `0` (or a negative /// error), which is where `user_data` should be released. Values already delivered remain valid /// until released with [moq_consume_json_value_free]. #[unsafe(no_mangle)] pub extern "C" fn moq_consume_json_cancel(task: u32) -> i32 { ffi::enter(move || { let task = ffi::parse_id(task)?; State::lock().consume.json_close(task) }) }