Skip to content

Add libmoq catalog producer + raw moq-net track API#1533

Merged
kixelated merged 4 commits into
mainfrom
claude/libmoq-catalog-producer-api-Z5I8x
May 29, 2026
Merged

Add libmoq catalog producer + raw moq-net track API#1533
kixelated merged 4 commits into
mainfrom
claude/libmoq-catalog-producer-api-Z5I8x

Conversation

@kixelated

@kixelated kixelated commented May 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds two related, additive pieces to libmoq's C API, both mirroring functionality that already exists in moq-ffi but was missing from the C bindings. Together they let a C caller describe a broadcast's catalog and write/read arbitrary track contents themselves, the producer-side opposite of the existing consume API.

1. Catalog producer API

The producer counterpart to moq_consume_video_config / moq_consume_audio_config. Lets callers author catalog renditions directly instead of relying on moq_publish_media_ordered to derive them.

  • moq_publish_video_config(broadcast, const moq_video_config*) / moq_publish_audio_config(broadcast, const moq_audio_config*) — insert or replace a rendition, keyed by name.
  • moq_publish_video_remove / moq_publish_audio_remove — remove by name.

These reuse the existing moq_video_config / moq_audio_config structs as inputs (the same structs the consume side fills as outputs), so producer and consumer are mirror images. Each call republishes the catalog automatically via the existing moq-mux catalog Producer guard. description / coded_width / coded_height pointers may be NULL to omit those fields.

2. Raw moq-net track API

The catalog API above only writes metadata. To actually feed a track, libmoq previously offered only moq_publish_media_ordered, which requires a known FramedFormat and owns its own catalog/container. There was no way to publish or consume a non-media track (control channel, JSON metadata, or your own encoded bitstream) over the C bindings.

This adds the bare moq-net primitives, mirroring moq-ffi's publish_track / subscribe_track (broadcast → track → group → frame, raw bytes, no codec/container/catalog):

Publish:

  • moq_publish_track(broadcast, name, name_len) → track handle (create_track)
  • moq_publish_track_group(track) → group handle (append_group)
  • moq_publish_track_frame(track, payload, len) — convenience one-frame-per-group write
  • moq_publish_group_frame(group, payload, len) / moq_publish_group_close(group)
  • moq_publish_track_close(track)

Consume:

  • moq_consume_track(broadcast, name, name_len, on_frame, user_data) → track handle
  • moq_consume_track_frame(frame, moq_frame*) — fills payload/payload_size; timestamp_us/keyframe are 0/false (not meaningful for raw tracks)
  • moq_consume_track_frame_close(frame) / moq_consume_track_close(track)

The consumer iterates groups in sequence order and delivers every frame within each group, so it sees exactly what the producer wrote regardless of grouping. Adds a GroupNotFound error code (-31).

These two pieces compose: describe a track with moq_publish_*_config, then push its frames with moq_publish_track/_frame.

Cross-package sync

Change in Also updated
rs/libmoq CHANGELOG.md; doc comments flow into the cbindgen-generated moq.h that doc/lib/c points at

Confined to rs/libmoq and purely additive (no existing signatures change), so it does not touch rs/moq-ffi, the wire/catalog formats, or the language wrappers — and targets main.

Test plan

  • cargo test -p libmoq — 19 tests pass, including:
    • publish_catalog_roundtrip: author video + audio renditions, publish, consume the catalog back (codec, coded dimensions, sample rate, channels), then remove the video rendition and confirm the republished catalog drops it.
    • raw_track_publish_consume: publish a raw data track, consume it via moq_consume_track, verify a one-frame-per-group write and a multi-frame group both round-trip byte-for-byte, and that timestamp_us/keyframe come back 0/false.
    • invalid-handle and null-pointer tests for both APIs.
  • cargo clippy -p libmoq --all-targets — clean.
  • cargo fmt -p libmoq -- --check — clean.
  • Generated moq.h picks up all new functions (one declaration each).

(Written by Claude)

claude added 2 commits May 29, 2026 01:50
Add the producer counterpart to the catalog consume API in libmoq.
Callers can now author catalog renditions directly instead of relying
on moq_publish_media_ordered to generate them:

- moq_publish_video_config / moq_publish_audio_config: insert or replace
  a rendition, keyed by name, reusing the existing moq_video_config /
  moq_audio_config structs as inputs.
- moq_publish_video_remove / moq_publish_audio_remove: remove a rendition
  by name.

Each call republishes the catalog automatically via the existing
moq-mux catalog Producer guard. Includes unit tests (invalid-handle,
null-pointer, and a publish/consume round-trip) and a changelog entry.
The generated C header picks up the new functions and their doc
comments via cbindgen.
- Drop the identity .into() on hang::Error in the catalog producer
  map_err calls (clippy useless_conversion).
- Reflow the producer method signatures to satisfy rustfmt.
- Record the new catalog producer functions under [Unreleased] in the
  libmoq changelog.
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@kixelated, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 12 minutes and 31 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 63bf5e3f-df79-4237-bfac-49628f7b5630

📥 Commits

Reviewing files that changed from the base of the PR and between 37b394a and b418df3.

📒 Files selected for processing (1)
  • rs/libmoq/src/test.rs

Walkthrough

This PR adds two families of changes: (1) catalog-rendition producer APIs—Rust Publish methods and C-ABI exports—to insert/replace and remove named video/audio renditions (with CHANGELOG and tests), and (2) a raw (non-media) moq-net track path—Publish storage for Track/Group producers, publish FFI for track/group/frame lifecycle, Consume runtime and FFI for subscribing to raw tracks (async task, frame buffering, frame read/close), an added Error::GroupNotFound mapping, and tests covering invalid ids and end-to-end raw-group/frame delivery.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main changes: adding both a catalog producer API and raw moq-net track API to libmoq.
Description check ✅ Passed The description comprehensively explains both the catalog producer API and raw track API additions, with clear API signatures, use cases, and testing information.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/libmoq-catalog-producer-api-Z5I8x

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

The previous review aborted because I pushed a follow-up commit (clippy useless_conversion + rustfmt fixes) while the review was in flight. The branch is settled now at eec70b9. Thanks!

(Written by Claude)


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

@kixelated Sure! I'll kick off a fresh review of the current branch state at eec70b9.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rs/libmoq/src/api.rs`:
- Around line 391-394: The code treats a NULL description pointer as "omit" even
when config.description_len > 0; change both places that check
config.description and call ffi::parse_slice (the blocks that set
video.description) to explicitly reject a NULL pointer when description_len > 0
by returning the appropriate ffi::InvalidPointer error (or equivalent) instead
of skipping parsing; otherwise, proceed to call
ffi::parse_slice(config.description, config.description_len)? and set
video.description = Some(bytes::Bytes::copy_from_slice(...)).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 60763b12-21d3-46be-afb3-1c22c24a32b6

📥 Commits

Reviewing files that changed from the base of the PR and between ad5b3ed and eec70b9.

📒 Files selected for processing (4)
  • rs/libmoq/CHANGELOG.md
  • rs/libmoq/src/api.rs
  • rs/libmoq/src/publish.rs
  • rs/libmoq/src/test.rs

Comment thread rs/libmoq/src/api.rs
Comment on lines +391 to +394
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));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject NULL descriptions when description_len > 0.

Both config entry points currently treat description = NULL as “omit the field” even when description_len is non-zero. That silently drops malformed FFI input instead of surfacing InvalidPointer, which makes interop bugs much harder to diagnose.

Suggested validation
-		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));
-		}
+		match (config.description.is_null(), config.description_len) {
+			(true, 0) => {}
+			(true, _) => return Err(Error::InvalidPointer),
+			(false, _) => {
+				let description = unsafe { ffi::parse_slice(config.description, config.description_len)? };
+				video.description = Some(bytes::Bytes::copy_from_slice(description));
+			}
+		}
...
-		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));
-		}
+		match (config.description.is_null(), config.description_len) {
+			(true, 0) => {}
+			(true, _) => return Err(Error::InvalidPointer),
+			(false, _) => {
+				let description = unsafe { ffi::parse_slice(config.description, config.description_len)? };
+				audio.description = Some(bytes::Bytes::copy_from_slice(description));
+			}
+		}

Also applies to: 429-432

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rs/libmoq/src/api.rs` around lines 391 - 394, The code treats a NULL
description pointer as "omit" even when config.description_len > 0; change both
places that check config.description and call ffi::parse_slice (the blocks that
set video.description) to explicitly reject a NULL pointer when description_len
> 0 by returning the appropriate ffi::InvalidPointer error (or equivalent)
instead of skipping parsing; otherwise, proceed to call
ffi::parse_slice(config.description, config.description_len)? and set
video.description = Some(bytes::Bytes::copy_from_slice(...)).

Expose the bare moq-net track primitives over the C FFI so callers can
publish and consume arbitrary (non-media) byte tracks, with no codec,
container, or catalog framing. This mirrors moq-ffi's publish_track /
subscribe_track and pairs with the catalog producer API: describe a
track with moq_publish_*_config, then feed it yourself.

Publish side:
- moq_publish_track(broadcast, name) -> track handle (create_track)
- moq_publish_track_group(track) -> group handle (append_group)
- moq_publish_track_frame(track, payload) convenience one-frame group
- moq_publish_group_frame(group, payload) / moq_publish_group_close(group)
- moq_publish_track_close(track)

Consume side:
- moq_consume_track(broadcast, name, on_frame, user_data) -> track handle
- moq_consume_track_frame(frame, dst) fills moq_frame (timestamp/keyframe
  are 0/false for raw tracks)
- moq_consume_track_frame_close(frame) / moq_consume_track_close(track)

The consumer iterates groups in sequence order and delivers every frame
within each group, so the consumer sees exactly what the producer wrote
regardless of grouping. Adds a GroupNotFound error code (-31), a
publish/consume round-trip test, an invalid-handle test, and changelog
entries.
@kixelated kixelated changed the title Add libmoq catalog producer API Add libmoq catalog producer + raw moq-net track API May 29, 2026
The earlier test added a catalog rendition + catalog-subscribe handshake
on the theory that a frame written before the subscription attached would
be dropped. That was wrong: moq-net caches the latest group and
subscribers replay from the first frame of each group, so a late
subscriber still sees it.

The original failure was actually the run_raw consumer reading only the
first frame of each group (it used read_frame, which skips the rest of
the group), so the multi-frame group hung. That's already fixed by
iterating every frame per group. Drop the handshake; the test passes
reliably without it.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
rs/libmoq/src/publish.rs (1)

144-148: 💤 Low value

Group producers can leak from the groups slab.

track_group inserts a GroupProducer into self.groups, but track_finish only removes the track. Any groups created from that track and not explicitly closed via group_finish remain in the slab for the lifetime of Publish. Not user-visible, but it accumulates handles for the common author-then-finish flow. Consider tracking group ownership per-track so finishing a track also drops its outstanding groups.

Also applies to: 159-163


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c033a0b5-b90d-4b20-b01d-a1eb621cc670

📥 Commits

Reviewing files that changed from the base of the PR and between eec70b9 and 37b394a.

📒 Files selected for processing (6)
  • rs/libmoq/CHANGELOG.md
  • rs/libmoq/src/api.rs
  • rs/libmoq/src/consume.rs
  • rs/libmoq/src/error.rs
  • rs/libmoq/src/publish.rs
  • rs/libmoq/src/test.rs
✅ Files skipped from review due to trivial changes (1)
  • rs/libmoq/CHANGELOG.md

@kixelated kixelated merged commit e35a9b2 into main May 29, 2026
1 check passed
@kixelated kixelated deleted the claude/libmoq-catalog-producer-api-Z5I8x branch May 29, 2026 03:12
This was referenced May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants