Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 161 additions & 50 deletions doc/rs/crate/libmoq.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ description: C bindings for MoQ

[![docs.rs](https://docs.rs/libmoq/badge.svg)](https://docs.rs/libmoq)

C bindings for `moq-lite` via FFI, enabling MoQ integration in C/C++ applications and other languages.
C bindings for MoQ via FFI, providing media publish/subscribe functionality for C/C++ applications and other languages.

## Overview

`libmoq` provides:
`libmoq` provides a C API for real-time media delivery over QUIC. It wraps the Rust [moq-lite](/rs/crate/moq-lite) and [moq-mux](/rs/crate/moq-mux) crates, handling:

- **C API** - Header files for C integration
- **FFI bindings** - Safe Rust-to-C interface
- **Build system integration** - CMake and pkg-config support
- **Sessions** - QUIC/WebTransport connections to MoQ relays
- **Origins** - Containers for broadcast discovery and routing
- **Publishing** - Encoding and sending audio/video tracks
- **Consuming** - Receiving, decoding, and rendering media tracks
- **Catalogs** - Discovering available audio/video renditions

All functions use opaque integer handles to reference resources. Negative return values indicate errors, zero indicates success, and positive values are resource handles.

## Installation

Expand All @@ -27,76 +31,183 @@ cd moq/rs/libmoq
cargo build --release
```

The library will be in `target/release/libmoq.a` (static) or `target/release/libmoq.so` (dynamic).
The static library will be at `target/release/libmoq.a`.

### Linking

With CMake:

### Using Cargo
```cmake
find_package(moq REQUIRED)
target_link_libraries(myapp moq)
```

With pkg-config:

```bash
cargo install libmoq
gcc -o myapp myapp.c $(pkg-config --cflags --libs moq)
```

## Usage
## API

### C Header
### Initialization

```c
#include <moq.h>
| Function | Description |
|----------|-------------|
| `moq_log_level(level, level_len)` | Set log level: `"error"`, `"warn"`, `"info"`, `"debug"`, `"trace"` |

int main() {
// Initialize connection
moq_connection_t* conn = moq_connect("https://relay.example.com/demo");
if (!conn) {
fprintf(stderr, "Failed to connect\n");
return 1;
}
### Sessions

// Create broadcast
moq_broadcast_t* broadcast = moq_broadcast_new("my-broadcast");
Connect to a MoQ relay server over QUIC/WebTransport.

// Create track
moq_track_t* track = moq_track_new(broadcast, "chat");
| Function | Description |
|----------|-------------|
| `moq_session_connect(url, url_len, origin_publish, origin_consume, on_status, user_data)` | Connect to a relay. Provide origin handles for publish/consume, or `0` to disable. Calls `on_status` on connect (code 0) and close (code non-zero). |
| `moq_session_close(session)` | Close a session and cancel its background task. |

// Publish data
moq_group_t* group = moq_group_append(track);
moq_frame_write(group, "Hello, MoQ!", 11);
moq_group_close(group);
### Origins

// Publish to relay
moq_publish(conn, broadcast);
Origins group broadcasts by path. They can be shared across sessions for fanout/relaying.

// Cleanup
moq_broadcast_free(broadcast);
moq_connection_free(conn);
| Function | Description |
|----------|-------------|
| `moq_origin_create()` | Create a new origin. |
| `moq_origin_publish(origin, path, path_len, broadcast)` | Publish a broadcast to an origin at the given path. |
| `moq_origin_consume(origin, path, path_len)` | Consume a broadcast from an origin by path. Returns a broadcast handle. |
| `moq_origin_announced(origin, on_announce, user_data)` | Discover broadcasts published to an origin. Calls `on_announce` with an announced ID for each broadcast. |
| `moq_origin_announced_info(announced, dst)` | Query the path and active status of an announced broadcast. |
| `moq_origin_announced_close(announced)` | Stop listening for announcements. |
| `moq_origin_close(origin)` | Close an origin. |

return 0;
}
```
### Publishing

### Linking
Create broadcasts and write media frames.

With CMake:
| Function | Description |
|----------|-------------|
| `moq_publish_create()` | Create a new broadcast for publishing. |
| `moq_publish_media_ordered(broadcast, format, format_len, init, init_size)` | Add a media track to a broadcast. `format` specifies the encoding. `init` is codec-specific initialization data. |
| `moq_publish_media_frame(media, payload, payload_size, timestamp_us)` | Write a frame to a media track. Frames must be in decode order. Timestamp is in microseconds. |
| `moq_publish_media_close(media)` | Remove a media track from a broadcast. |
| `moq_publish_close(broadcast)` | Close a broadcast. |

```cmake
find_package(moq REQUIRED)
target_link_libraries(myapp moq)
### Consuming

Subscribe to broadcasts and receive decoded media frames.

| Function | Description |
|----------|-------------|
| `moq_consume_catalog_subscribe(broadcast, on_catalog, user_data)` | Subscribe to catalog updates. Calls `on_catalog` with a catalog snapshot ID when the catalog changes. |
| `moq_consume_catalog_unsubscribe(catalog)` | Stop the catalog subscription background task. Previously delivered snapshots remain valid. |
| `moq_consume_catalog_close(catalog)` | Close a catalog snapshot. Invalidates any borrowed pointers from config queries. |
| `moq_consume_video_config(catalog, index, dst)` | Query video rendition info: name, codec, description, dimensions. |
| `moq_consume_audio_config(catalog, index, dst)` | Query audio rendition info: name, codec, description, sample rate, channels. |
| `moq_consume_video_ordered(catalog, index, max_latency_ms, on_frame, user_data)` | Subscribe to a video track. Delivers frames in order, skipping GoPs when latency exceeds `max_latency_ms`. |
| `moq_consume_audio_ordered(catalog, index, max_latency_ms, on_frame, user_data)` | Subscribe to an audio track. Same latency behavior as video. |
| `moq_consume_video_close(track)` | Close a video track subscription. |
| `moq_consume_audio_close(track)` | Close an audio track subscription. |
| `moq_consume_frame_chunk(frame, index, dst)` | Read a chunk of frame payload. Call with increasing `index` to get all chunks. |
| `moq_consume_frame_close(frame)` | Close a frame and release its memory. |
| `moq_consume_close(consume)` | Close a broadcast consumer. |

### Data Structures

```c
// Video rendition configuration
typedef struct {
const char *name; // Track name (NOT null-terminated)
size_t name_len;
const char *codec; // Codec string (NOT null-terminated)
size_t codec_len;
const uint8_t *description; // Codec-specific init data, or NULL
size_t description_len;
const uint32_t *coded_width; // Encoded width, or NULL
const uint32_t *coded_height; // Encoded height, or NULL
} moq_video_config;

// Audio rendition configuration
typedef struct {
const char *name;
size_t name_len;
const char *codec;
size_t codec_len;
const uint8_t *description; // Codec-specific init data, or NULL
size_t description_len;
uint32_t sample_rate; // Sample rate in Hz
uint32_t channel_count; // Number of channels
} moq_audio_config;

// A frame of media data
typedef struct {
const uint8_t *payload; // Frame data, or NULL if stream ended
size_t payload_size;
uint64_t timestamp_us; // Presentation timestamp in microseconds
bool keyframe; // True if this starts a new GoP
} moq_frame;

// An announced broadcast
typedef struct {
const char *path; // Broadcast path (NOT null-terminated)
size_t path_len;
bool active; // Whether the broadcast is currently active
} moq_announced;
```

With pkg-config:
## Usage Example

```bash
gcc -o myapp myapp.c $(pkg-config --cflags --libs moq)
### Publishing

```c
#include <moq.h>

// Initialize logging
moq_log_level("info", 4);

// Create origin and session
int origin = moq_origin_create();
int session = moq_session_connect(url, url_len, origin, 0, on_status, NULL);

// Create a broadcast with a video track
int broadcast = moq_publish_create();
int video = moq_publish_media_ordered(broadcast, "h264", 4, init_data, init_size);

// Write frames
moq_publish_media_frame(video, frame_data, frame_size, timestamp_us);

// Publish to the relay
moq_origin_publish(origin, "my-stream", 9, broadcast);
```

## API Reference
### Consuming

```c
// Create origin and connect
int origin = moq_origin_create();
int session = moq_session_connect(url, url_len, 0, origin, on_status, NULL);

Full API documentation: [docs.rs/libmoq](https://docs.rs/libmoq)
// Consume a broadcast
int broadcast = moq_origin_consume(origin, "my-stream", 9);

## Use Cases
// Subscribe to the catalog
moq_consume_catalog_subscribe(broadcast, on_catalog, NULL);

- **C/C++ applications** - Native integration without Rust toolchain
- **Language bindings** - Build bindings for Python, Go, etc.
- **Legacy systems** - Integrate MoQ into existing C codebases
- **Embedded systems** - Where Rust runtime isn't available
// In the catalog callback:
void on_catalog(void *user_data, int catalog) {
moq_video_config config;
moq_consume_video_config(catalog, 0, &config);

// Subscribe to the first video track
moq_consume_video_ordered(catalog, 0, 500, on_frame, NULL);
}

// In the frame callback:
void on_frame(void *user_data, int frame) {
moq_frame chunk;
moq_consume_frame_chunk(frame, 0, &chunk);
// process chunk.payload, chunk.timestamp_us, chunk.keyframe
moq_consume_frame_close(frame);
}
```

## Next Steps

Expand Down
4 changes: 2 additions & 2 deletions rs/libmoq/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ int32_t moq_publish_media_frame(uint32_t media, const uint8_t *payload, uintptr_
int32_t moq_consume_close(uint32_t consume);

// Consuming: Catalog
int32_t moq_consume_catalog(uint32_t broadcast, void (*on_catalog)(void *user_data, int32_t catalog), void *user_data);
int32_t moq_consume_catalog_subscribe(uint32_t broadcast, void (*on_catalog)(void *user_data, int32_t catalog), void *user_data);
int32_t moq_consume_catalog_unsubscribe(uint32_t catalog);
int32_t moq_consume_catalog_close(uint32_t catalog);
int32_t moq_consume_catalog_free(uint32_t catalog);
int32_t moq_consume_video_config(uint32_t catalog, uint32_t index, moq_video_config *dst);
int32_t moq_consume_audio_config(uint32_t catalog, uint32_t index, moq_audio_config *dst);

Expand Down
28 changes: 14 additions & 14 deletions rs/libmoq/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,54 +360,54 @@ pub unsafe extern "C" fn moq_publish_media_frame(
})
}

/// Create a catalog consumer for a broadcast.
/// Subscribe to catalog updates for a broadcast.
///
/// The callback is called with a catalog ID when a new catalog is available.
/// The catalog ID can be used to query video/audio track information.
///
/// Returns a non-zero handle on success, or a negative code on failure.
///
/// # Safety
/// - The caller must ensure that `on_catalog` is valid until [moq_consume_catalog_close] is called.
/// - The caller must ensure that `on_catalog` is valid until [moq_consume_catalog_unsubscribe] is called.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn moq_consume_catalog(
pub unsafe extern "C" fn moq_consume_catalog_subscribe(
broadcast: u32,
on_catalog: Option<extern "C" fn(user_data: *mut c_void, catalog: i32)>,
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)
State::lock().consume.catalog_subscribe(broadcast, on_catalog)
})
}

/// Close a catalog consumer and cancel its background task.
/// Unsubscribe from catalog updates and cancel the background task.
///
/// This only stops the background subscription; catalog snapshots previously
/// delivered via the [moq_consume_catalog] callback remain valid until freed
/// with [moq_consume_catalog_free].
/// delivered via the [moq_consume_catalog_subscribe] callback remain valid until closed
/// with [moq_consume_catalog_close].
///
/// Returns a zero on success, or a negative code on failure.
#[unsafe(no_mangle)]
pub extern "C" fn moq_consume_catalog_close(catalog: u32) -> i32 {
pub extern "C" fn moq_consume_catalog_unsubscribe(catalog: u32) -> i32 {
ffi::enter(move || {
let catalog = ffi::parse_id(catalog)?;
State::lock().consume.catalog_close(catalog)
State::lock().consume.catalog_unsubscribe(catalog)
})
}

/// Free a catalog snapshot received via the [moq_consume_catalog] callback.
/// Close a catalog snapshot received via the [moq_consume_catalog_subscribe] 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 {
pub extern "C" fn moq_consume_catalog_close(catalog: u32) -> i32 {
ffi::enter(move || {
let catalog = ffi::parse_id(catalog)?;
State::lock().consume.catalog_free(catalog)
State::lock().consume.catalog_close(catalog)
})
}

Expand All @@ -419,7 +419,7 @@ pub extern "C" fn moq_consume_catalog_free(catalog: u32) -> i32 {
///
/// # 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.
/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_close] 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 || {
Expand All @@ -438,7 +438,7 @@ pub unsafe extern "C" fn moq_consume_video_config(catalog: u32, index: u32, dst:
///
/// # 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.
/// - The caller must ensure that `dst` is not used after [moq_consume_catalog_close] 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 || {
Expand Down
6 changes: 3 additions & 3 deletions rs/libmoq/src/consume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl Consume {
self.broadcast.insert(broadcast)
}

pub fn catalog(&mut self, broadcast: Id, on_catalog: OnStatus) -> Result<Id, Error> {
pub fn catalog_subscribe(&mut self, broadcast: Id, on_catalog: OnStatus) -> Result<Id, Error> {
let broadcast = self.broadcast.get(broadcast).ok_or(Error::BroadcastNotFound)?.clone();
let catalog = broadcast.subscribe_track(&hang::catalog::Catalog::default_track())?;

Expand Down Expand Up @@ -188,7 +188,7 @@ impl Consume {
Ok(())
}

pub fn catalog_close(&mut self, catalog: Id) -> Result<(), Error> {
pub fn catalog_unsubscribe(&mut self, catalog: Id) -> Result<(), Error> {
// Take the entire entry: drops the sender (signals shutdown) and revokes the callback.
self.catalog_task
.get_mut(catalog)
Expand All @@ -198,7 +198,7 @@ impl Consume {
Ok(())
}

pub fn catalog_free(&mut self, catalog: Id) -> Result<(), Error> {
pub fn catalog_close(&mut self, catalog: Id) -> Result<(), Error> {
self.catalog.remove(catalog).ok_or(Error::CatalogNotFound)?;
Ok(())
}
Expand Down
Loading
Loading