Skip to content

feat(moq-c)!: parity with moq-ffi for the cut - #8060

Merged
kixelated merged 20 commits into
mainfrom
quest/m2/cut-gate/c/README
Nov 8, 2026
Merged

kixelated merged 20 commits into
mainfrom
quest/m2/cut-gate/c/README

Conversation

@kixelated

@kixelated kixelated commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

Finishes quest/m2/cut-gate/c/README.md: moq-c gains every binding feature moq-ffi added in m1 and m2 (since the release fork at d0b086e). The C interop client, C fixtures, and doc/lib/c samples were checked against the built release archive.

Summary

Diffed moq-ffi's exports and record fields at the fork against main, then against moq.h, and added what C lacked:

  • In-band auth: moq_session_tokens_set, moq_session_tokens_answered, moq_session_grants (+ _cancel), moq_grant_info, moq_grant_free.
  • Sockets: moq_socket_* endpoints, with tcp and track dial, accept, incoming, and stream calls. A stream's write half is an existing group handle (tcp) or raw track handle (track), so the moq_publish_group_* / moq_publish_track_* calls write it. Its read half reads like moq_consume_track.
  • Codec-only encoders: moq_audio_encoder_* and moq_video_encoder_* encode without a broadcast, and hand back packets as moq_frame.
  • Publish: moq_publish_group_ready (backpressure), moq_publish_track_largest, moq_publish_timebase (+ _free) with moq_container_init.timebase, moq_encode_audio_reanchor.
  • Consume: moq_consume_closed (+ _cancel).
  • Decode:
    • moq_audio_decoder_output.fixed gives a fixed playout delay instead of the estimate, and moq_audio_read.delay_us reports it.
    • moq_video_decoder_output.surface with moq_video_frame.surface hands out the decoder's CVPixelBufferRef on Apple.
  • Catalog:
    • Rendition warmup_us on video and audio configs, and jitter_us on audio.
    • Rendition references broadcast, instance, and track on moq_video_config / moq_audio_config, on both the publish and consume sides.
  • Config:
    • moq_client_config.once dials once without reconnecting.
    • moq_server_config gains versions and the QUIC knobs the client already had.
  • Errors: new MOQ_ERROR_UNSUPPORTED (-46).

Also fixed:

  • A moq_publish_group_ready wait that parked before moq_publish_group_finish never resolved, because moq-net's poll_ready does not wake on a finish. Ending the handle now resolves it.
  • rs/moq-c/README.md cited moq_consume_frame/_free, which no longer exist.
  • The docs no longer say C lacks in-band auth or the playout delay. doc/lib/index.md now names the one remaining C gap, single-group fetch.

Fixed from review threads:

  • A socket endpoint no longer keeps every finished track writer until moq_socket_close. Its family drops finished producers, as moq-ffi's from_socket does, and still ends open ones on close.
  • A grant watch ends with a negative code when the session fails, and with 0 when it closes or is cancelled. Before, every end read as clean.
  • moq_consume_video/_audio and moq_decode_video/_audio enforce a rendition's instance when resolving its broadcast. The decoders also now follow a rendition's broadcast reference at all, which they previously ignored.
  • An accept loop's buffered first dial can no longer be delivered after moq_socket_cancel.
  • A moq_decode_audio/_video cancel while the rendition's sibling broadcast is still unannounced (or SUBSCRIBE_OK is pending) now ends the task with terminal 0. Before, the setup never polled the cancel, so the task and its user_data leaked.
  • Docs: the C page says that live token replacement needs AUTH, the m3 quest scopes "parity" to m1/m2 gains, and moq_publish_group_ready says how a broadcast close affects the wait.

Public API impact

Breaking ABI (!). These structs gained trailing fields, so their size and layout changed:

Struct New fields
moq_client_config once
moq_server_config versions, QUIC fields
moq_video_config warmup, reference
moq_audio_config jitter, warmup, reference
moq_container_init timebase
moq_audio_decoder_output fixed
moq_audio_read delay_us
moq_video_decoder_output surface
moq_video_frame surface

Migration: rebuild against the new moq.h. Code that zero-initializes these structs (= {0} or memset) needs no other change, because zero means the old behavior for every new field:

  • reconnect stays on;
  • no warmup, jitter or reference;
  • no timebase;
  • the audio delay stays estimated;
  • frames stay on the CPU.

A caller filling moq_video_config / moq_audio_config by hand without zeroing must set the new reference pointers to NULL.

Wire: none.

Choices made without asking

  • Socket halves are take-once. moq_socket_stream_writer and moq_socket_stream_reader each succeed once. moq_socket_stream_free drops any half not taken, and moq-net resets the session for an unfinished tcp writer. A track writer is a child of its endpoint, so moq_socket_close ends it, the same way a broadcast's close ends its tracks.
  • Accept starts listening immediately. moq_socket_accept_* polls once before returning, so a dial sent right after the call queues for the loop instead of finding nobody listening. A stress run caught that race.
  • A declared-end track's groups outlive the broadcast close. A track ended with moq_publish_track_finish_at is not aborted when its broadcast closes, matching moq-ffi, so a moq_publish_group_ready wait on its group resolves when a reader takes the group or the group ends. A test pins this, and the doc states it.
  • No cancel for one-shot waits. moq_session_tokens_answered and moq_publish_group_ready always resolve (on the connection's end or the group's end), so neither has a cancel call.
  • An accepted session presents no tokens. moq_session_tokens_* returns MOQ_ERROR_UNSUPPORTED for one.
  • Surfaces are Apple only. A surface decode pins VideoToolbox with no software fallback, and ignores format, width and height. Elsewhere, surface = true fails up front with MOQ_ERROR_UNSUPPORTED. The Apple path is not exercised on this Linux host.
  • Rendition broadcast came along with instance and track. moq-ffi had broadcast before the fork, but instance and track mean nothing without it.
  • CHANGELOG and version untouched. The CHANGELOG is release-generated. The version stays 0.7.0, because the shared 0.22 minor is the cut's manual bump.
  • The release path checks out. release-plz tags moq-c-v*, which runs .github/workflows/moq-c.yml, which runs just rs package-moq-c (rs/moq-c/build.sh). That is the archive validated below.

Out of scope: features moq-c never had

These moq-ffi features predate the fork, and moq-c never had them:

  • group-level consumption and single-group fetch (MoqTrackConsumer::recv_group/next_group, MoqGroupConsumer, MoqBroadcastConsumer::fetch_group, MoqMediaGroupConsumer);
  • MoqRequest::url/transport;
  • rendition bitrate on moq_*_config.

The quest's plan scopes the gate to m1 and m2 gains, so I left them out. Recommendation: leave them to the generated C line (quest/m3/c/README.md), which replaces this crate after the cut and gets them for free. Say so if you want any of them in the hand-written crate first.

In-flight PRs (not duplicated here)

Each of these changes rs/moq-c alongside its own binding change, and closes its own C gap:

Follow-ups

  • moq-net: group::Producer::poll_ready never wakes on finish when no reader holds the group. moq-c now works around it. moq-ffi's MoqGroupProducer::ready has the same hang.
  • The C interop client could print the grant, as the other clients do, now that C has moq_session_grants.
  • The out-of-scope features above, if wanted before the generated line.

Validation

  • cargo nextest run -p moq-c --features openh264: 194 passed. The new test::parity tests ran 200 stress iterations clean.
  • Each review-round fix has a test that fails without it: socket endpoint retention, instance refusal, decoded sibling reference, the parked group-ready wake, and the decoder cancel during a sibling resolve.
  • just ci (via quest gate) on e364663: pass.
  • cargo nextest run -p moq-c --features openh264: 195 passed.
  • just check: pass.
  • just rs c-tests: pass.
  • Release archive from rs/moq-c/build.sh (nix, x86_64-linux):
    • All 245 functions moq.h declares are defined in libmoq.a.
    • Every rs/moq-c/c-tests fixture compiles with -Wall -Werror and runs linked through the archive's pkg-config.
    • The doc/lib/c samples syntax-check against its header.
    • Every moq_/MOQ_ name cited in doc/lib/c/index.md and rs/moq-c/README.md resolves.
    • All 147 README prototypes compile as redeclarations against the header.
    • test/interop/clients/c/subscribe.c compiles and links.
  • test/interop/interop.sh --publishers rust --subscribers c: all checks passed (media, grant, swapped tones, denied publisher, finished tail).

Review

Rewritten by quest review after each round.

  • Reviewer: Codex, gpt-6.1-sol, medium effort
  • Session: 01a1b9c4-5840-7a72-bc92-e082fcba8a6e, round 1
  • Head: ec6cb0d (e364663 adds only a conflict-free merge of main)
  • Base: origin/main at 157283d
  • Score: 8/10, Source review found no actionable correctness defects or blockers, but runtime behavior remains unverified because builds and tests were not run, as instructed.
  • Findings: none
  • The previous session (01a1b89c-d73a-7f32-96fd-74d90a0575e2, round 3, at a2205c3) scored 10/10 with no findings.

Models

  • Implementer: Claude Code, claude-opus-5-5 (iterate, M)
  • Implementer: Claude Code, claude-opus-5-5, medium effort (iterate, S)

(Written by Claude Opus 5.5 with medium)

🤖 Generated with Claude Code

kixelated and others added 11 commits November 6, 2026 11:50
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title moq-c at parity for the cut feat(moq-c)!: parity with moq-ffi for the cut Nov 6, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review November 6, 2026 21:10

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Automated review by review (OpenAI)

Reviewed head: 4a486d0

One actionable finding: finished socket track writers are retained until the entire endpoint closes. Repeated track sessions on a long-lived endpoint therefore accumulate completed producer state. See the inline P2 for the ownership path and suggested fix.

Direction: keep endpoint-close tracking for open socket writers, but release finished socket producers rather than retaining them as broadcast tracks. Add a regression test that repeatedly opens, finishes, and releases track streams while keeping the endpoint open, and verifies completed producers are released.

Coverage: all 25 changed files, including socket/auth lifecycle and cancellation, standalone encoder packet ownership, catalog references and timebases, group readiness, consume closure, client/server configuration, and decoder output changes. The current surface backend selection and ignored CPU-output fields address the prior findings described in the PR.

Limits: GitHub-only static review. I did not run builds, tests, quest context, or native Apple validation, and have not independently reproduced the validation results listed in the PR. State, draft status, full head SHA, and existing reviews were rechecked immediately before posting.

Comment thread rs/moq-c/src/publish.rs Resolved

@pullfrog pullfrog 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.

Important

The open P2 on finished socket-track retention is still unfixed on this head — I independently confirmed the retaining path (details below). One user-facing doc page also becomes wrong once this merges.

Reviewed changes

  • In-band auth (auth.rs) — tokens set/answered, grant watchers over Handle::grant(), and moq_grant_info/free, with grant records owning the strings they lend.
  • Sockets (socket.rs) — endpoint create/close, tcp and track dial/accept, incoming accept/reject/info, and take-once stream halves whose tcp writer is a group handle and track writer a raw track handle.
  • Codec-only encoders (encoder.rs) — moq_audio_encoder_* / moq_video_encoder_* encoding without a broadcast and handing back moq_frame packets.
  • Publish — moq_publish_group_ready plus the per-group watch that resolves a wait a group finish does not wake, moq_publish_track_largest, moq_publish_timebase(_free) with moq_container_init.timebase, moq_encode_audio_reanchor.
  • Consume — moq_consume_closed(_cancel), run_group split out of run_raw, and RawControl::Group refusing a subscription change on a tcp half.
  • Decode — moq_audio_decoder_output.fixed with moq_audio_read.delay_us, and the Apple-only moq_video_decoder_output.surface / moq_video_frame.surface path that pins VideoToolbox.
  • Catalog and config — rendition warmup_us / jitter_us and broadcast/instance/track references on both sides; moq_client_config.once; moq_server_config.versions and the QUIC knobs via a shared client::Quic.
  • Errors — MOQ_ERROR_UNSUPPORTED (-46), mapping moq_net::Error::Unsupported and a new Error::Unsupported.
  • Docs, quest, tests — C token paragraph and gap line, README prototypes, quest deletion with link fixups, and the new test::parity suite.

⚠️ Finished socket tracks are retained by the endpoint until it closes

This is the same issue as the open P2 thread, and it still holds on 4a486d0. moq_socket_stream_writer wraps a track half in the broadcast-style Kept, so moq_publish_track_finish reaches Children::keep (rs/moq-c/src/publish.rs:157), which appends the finished moq_net::track::Producer to the endpoint's kept vector. Only moq_socket_close releases that vector, so a long-lived endpoint that accepts and finishes many track sessions grows unboundedly, and the track's last-producer teardown never runs. Socket tracks have no late subscriber to keep the producer for — moq-ffi's MoqTrackProducer::from_socket sets keep: None for exactly this case.

Technical details
# Release finished socket tracks instead of retaining them

## Affected sites
- `rs/moq-c/src/publish.rs:1159` — `socket_track` builds `Kept { inner, children }`, opting the handle into `Children::keep`.
- `rs/moq-c/src/publish.rs:205` — `Kept::keep` calls `children.keep(child, inner)`.
- `rs/moq-c/src/publish.rs:157` — `Children::keep` pushes the producer onto `kept`, which only `Children::close` drops.
- `rs/moq-c/src/socket.rs:122` — the endpoint's `children` (closed only by `moq_socket_close`) is what stream halves clone.

## Required outcome
- A finished socket track releases its producer at `moq_publish_track_finish` (as `moq-ffi` does), while an *open* socket track is still ended by `moq_socket_close`.
- Keep the endpoint-close tracking, drop only the finished-producer retention.
- Add a regression test that opens, finishes, and releases track streams repeatedly without closing the endpoint, and asserts the completed producer was released (e.g. `Children`/`Shared` holder count, as `Shared::holders` already enables).

## Suggested approach (optional)
Give `Children::insert` a flag (or a second `insert_unkept`) so `socket_track` uses the open-child path without `keep` on finish; have `track_finish` drop instead of `keep` for children created that way.

ℹ️ doc/lib/layers.md still says C has no codec-only encoder

doc/lib/layers.md:24 ends with "the codecs are moq_encode_* and moq_decode_*, which always publish to or subscribe from a broadcast, so C has no codec-only Encoder", and the audio / video C rows (lines 107 and 123) name only moq_encode_* / moq_decode_*. This PR adds moq_audio_encoder_* and moq_video_encoder_*, so all three lines become false; doc/lib/index.md was updated for the other two C gaps but this page was missed.

Technical details
# layers.md contradicts the new codec-only C encoders

## Affected sites
- `doc/lib/layers.md:24` — asserts C has no codec-only `Encoder`.
- `doc/lib/layers.md:107` — audio C row prefixes omit `moq_audio_encoder_*`.
- `doc/lib/layers.md:123` — video C row prefixes omit `moq_video_encoder_*`.

## Required outcome
- The exception paragraph describes C's codec prefixes as covering both the broadcast-bound and the standalone encoder, with the correct prefixes.
- Both C rows list the standalone encoder prefix beside `moq_encode_*`, matching the pattern the other rows use.

ℹ️ The deleted quest is still named as a check in the recurring cut gate

Removing the ## Required line leaves the same work named in the gate's Goal (quest/m2/cut-gate/README.md:8: "and the hand-written moq-c is at parity with moq-ffi") and in the m2 epic prose (quest/m2/README.md:25 and :188). The gate's Plan says each recurrence "keeps this Goal", so the next cut would be re-created requiring parity for a crate the generated C line replaces after this cut.

Technical details
# Drop the completed check from the cut gate's Goal

## Affected sites
- `quest/m2/cut-gate/README.md:8` — Goal lists "the hand-written `moq-c` is at parity with moq-ffi" as a standing check.
- `quest/m2/README.md:25` — epic Plan lists "moq-c parity" among the gate's checks.
- `quest/m2/README.md:188` — the cut-gate entry's description ends "and moq-c at parity".

## Required outcome
- The gate's Goal and the m2 epic name only checks that still apply to a later cut, so a re-created gate does not require parity work that is finished and superseded.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

Comment thread rs/moq-c/src/auth.rs Resolved
@coderabbitai

coderabbitai Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

Walkthrough

The C binding adds session authorization, socket streams, standalone audio and video encoders, shared publication timebases, broadcast and group watchers, and expanded media configuration. Audio reads expose delay information, and video decoding can return native surfaces on Apple platforms. Tests and API documentation cover the added operations. Quest documentation updates the C parity references.

Merge Risk: Moderate · up to 4a486

Resolve the instance constraint, closure wakeup, and cancellation handling before merging unless explicitly accepted. Otherwise consumers can receive a different instance’s frames, readiness callbacks can remain pending, and canceled accepts can deliver incoming handles. Documentation also needs to qualify token replacement and parity scope.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 57.38% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed

Check name Status Explanation
Title check Passed The title identifies the main change: moq-c parity with moq-ffi for the cut.
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.
Description check Passed The description is specific and related to the changeset. It names the moq-c parity work, lists the added C API functions and struct fields (for example moq_session_tokens_set, moq_socket_*, moq_audio…
Full details: Description check

Explanation

The description is specific and related to the changeset. It names the moq-c parity work, lists the added C API functions and struct fields (for example moq_session_tokens_set, moq_socket_, moq_audio_encoder_, moq_video_encoder_*, and moq_publish_group_ready), and matches the raw summary. It also covers the ABI impact, migration steps, documentation changes, the removed quest README, and the validation runs. Nothing in it is off-topic, so the check passes.

Full details: Docstring Coverage

Explanation

Docstring coverage is 57.38% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 61 functions across 17 files. (5 skipped: 5 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches

Simplify code

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@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: 5

Nitpick comments: 5

rs/moq-c/src/encoder.rs: 2

46-48: Trivial · Maintainability & Code Quality · Quick win

Check packets returned by each encode call.

The tests count encode packets but do not read them. OpenH264 returns no packets at finish, so the video test can skip all packet checks.

For each positive encode count, check every packet before the next encode or finish call. Check the payload and, for video, the expected timestamp and keyframe flag.

Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

Review comment at @rs/moq-c/src/encoder.rs around lines 46 - 48:
Update the encode-test flows that use packet so every packet from each positive
encode count is checked before the next encode or finish call; verify its
payload and, for video, its expected timestamp and keyframe flag. Do not rely on
finish to return packets, since the video test must validate packets produced by
earlier encode calls.

361-367: Trivial · Functional Correctness · Quick win

Test the picture encoded after a cut.

The parity test cuts after its last picture, so a no-op cut could pass. Encode another picture away from a scheduled GOP boundary and assert that its access unit is a keyframe. Match the packet by timestamp and inspect packets from both encode and finish to handle pipelined backends.

Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

Review comment at @rs/moq-c/src/encoder.rs around lines 361 - 367:
Update the parity test for moq_video_encoder_cut to encode an additional picture
after cutting, at a timestamp away from a scheduled GOP boundary, and assert
that the matching access unit is a keyframe. Match packets by timestamp and
inspect outputs from both encode and finish so pipelined backends are covered.
rs/moq-c/src/test.rs: 1

5342-5343: Trivial · Maintainability & Code Quality · Low value

Pass the sibling reference through the C audio config.

Set broadcast and broadcast_len to ./source and remove the direct catalog mutation. The test can then detect an audio-specific regression that drops the C reference.

Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

Review comment at @rs/moq-c/src/test.rs around lines 5342 - 5343:
Update the C audio config’s broadcast and broadcast_len fields to pass the
./source sibling reference, and remove the test’s direct catalog mutation so it
exercises the C reference path.
rs/moq-c/src/test/parity.rs: 2

311-325: Trivial · Maintainability & Code Quality · Poor tradeoff

Test container timebase sharing and ownership.

This test only creates and frees handles. It does not check container rejection of a timebase from another broadcast or shared output timestamps. Add C API tests for both behaviors.

Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

Review comment at @rs/moq-c/src/test/parity.rs around lines 311 - 325:
Extend the C API coverage in `timebase_handles_are_owned_by_their_broadcast` to
verify that a container rejects a timebase created by a different broadcast and
that containers sharing a timebase produce the expected shared output
timestamps. Use the existing C API test helpers and preserve the current handle
ownership checks.

327-360: Trivial · Functional Correctness · Poor tradeoff

Test the effective server settings.

The test closes the listener without checking its configuration. It can pass if the server discards the valid version restriction and QUIC values. Add checks for an excluded protocol and for the effective stream limit and idle timeout.

Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

Review comment at @rs/moq-c/src/test/parity.rs around lines 327 - 360:
Extend client_once_and_server_quic_options_parse to verify the listener’s
effective server configuration before closing it: confirm the excluded protocol
is not negotiated and the configured QUIC stream limit and idle timeout are
applied. Use the existing test APIs or callbacks to inspect those effective
settings, and preserve the current valid-configuration and unknown-version
checks.

  • Fix comments: Fix CodeRabbit comments on this PR
Prompt to fix all issues
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. 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:
Review comments at @doc/lib/c/index.md:
- Line 108: Update the Tokens documentation beside the moq_session_tokens_set()
replacement instructions to state that, for moq-lite versions before 07,
replacement only applies to a later dial and cannot refresh the live session;
the answer reports MOQ_ERROR_UNSUPPORTED, and the relay may close the session
when its credential expires. Keep this limitation explicitly tied to that
version condition.

Review comments at @quest/m3/c/README.md:
- Line 15: Update the README wording around “parity” to clarify that the cut
covers the m1 and m2 binding additions, not full API parity. Explicitly note
that the hand-written C API cannot fetch a single group by sequence, while
moq-ffi can.

Review comments at @rs/moq-c/src/api.rs:
- Around line 3144-3150: Update media resolution used by moq_consume_video and
moq_consume_audio to accept and enforce the rendition’s instance, including when
resolving catalog-broadcast references. Reject a resolution whose broadcast
instance differs from the rendition instance, while preserving existing behavior
for matching instances.
- Around line 3882-3895: Update the readiness wait in
`moq_publish_track_finish_at` so broadcast closure also resolves a group’s
`poll_ready` wait, even when the track has a declared end and the group remains
unread. Preserve the existing readiness and handle-ending completion paths, and
use the existing broadcast-closure signal rather than assuming `ended.changed()`
represents it.

Review comments at @rs/moq-c/src/socket.rs:
- Around line 184-186: Update the accept task’s handling of `first` so it checks
for cancellation before delivering a buffered dial; if cancellation has
occurred, follow the existing terminal-callback path without yielding the
incoming handle. Preserve the existing handling of `first` when the caller has
not canceled.

---

Nitpick comments:
Review comments at @rs/moq-c/src/encoder.rs:
- Around line 46-48: Update the encode-test flows that use packet so every
packet from each positive encode count is checked before the next encode or
finish call; verify its payload and, for video, its expected timestamp and
keyframe flag. Do not rely on finish to return packets, since the video test
must validate packets produced by earlier encode calls.
- Around line 361-367: Update the parity test for moq_video_encoder_cut to
encode an additional picture after cutting, at a timestamp away from a scheduled
GOP boundary, and assert that the matching access unit is a keyframe. Match
packets by timestamp and inspect outputs from both encode and finish so
pipelined backends are covered.

Review comments at @rs/moq-c/src/test.rs:
- Around line 5342-5343: Update the C audio config’s broadcast and broadcast_len
fields to pass the ./source sibling reference, and remove the test’s direct
catalog mutation so it exercises the C reference path.

Review comments at @rs/moq-c/src/test/parity.rs:
- Around line 311-325: Extend the C API coverage in
`timebase_handles_are_owned_by_their_broadcast` to verify that a container
rejects a timebase created by a different broadcast and that containers sharing
a timebase produce the expected shared output timestamps. Use the existing C API
test helpers and preserve the current handle ownership checks.
- Around line 327-360: Extend client_once_and_server_quic_options_parse to
verify the listener’s effective server configuration before closing it: confirm
the excluded protocol is not negotiated and the configured QUIC stream limit and
idle timeout are applied. Use the existing test APIs or callbacks to inspect
those effective settings, and preserve the current valid-configuration and
unknown-version checks.

---

Failed pre-merge checks:
- Docstring Coverage:
Write docstrings for the functions missing them to satisfy the coverage
threshold.

After applying the fix, consider running `coderabbit review --agent` for local
review. Visit https://docs.coderabbit.ai/cli?utm_source=ghpr
Review info

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9a814846-b231-44d4-befc-70e9a0ccd6d6
Commits

Reviewing files that changed from the base of the PR and between 36cbc21 and 4a486d0.

Files selected for processing: 25

  • doc/lib/c/index.md
  • doc/lib/index.md
  • quest/m2/cut-gate/README.md
  • quest/m2/cut-gate/c/README.md
  • quest/m3/c/README.md
  • quest/m3/c/consumers.md
  • quest/m5/c-origin-relay.md
  • rs/moq-c/README.md
  • rs/moq-c/c-tests/decode-output.c
  • rs/moq-c/src/api.rs
  • rs/moq-c/src/audio.rs
  • rs/moq-c/src/auth.rs
  • rs/moq-c/src/client.rs
  • rs/moq-c/src/consume.rs
  • rs/moq-c/src/encoder.rs
  • rs/moq-c/src/error.rs
  • rs/moq-c/src/ffi.rs
  • rs/moq-c/src/lib.rs
  • rs/moq-c/src/publish.rs
  • rs/moq-c/src/session.rs
  • rs/moq-c/src/socket.rs
  • rs/moq-c/src/state.rs
  • rs/moq-c/src/test.rs
  • rs/moq-c/src/test/parity.rs
  • rs/moq-c/src/video.rs
Files with no reviewable changes: 1

  • quest/m2/cut-gate/c/README.md

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review.

Comment thread doc/lib/c/index.md Resolved
Comment thread quest/m3/c/README.md Resolved
Comment thread rs/moq-c/src/api.rs Resolved
Comment thread rs/moq-c/src/api.rs Resolved
Comment thread rs/moq-c/src/socket.rs Resolved
@kixelated
kixelated enabled auto-merge November 6, 2026 23:10
@kixelated

Copy link
Copy Markdown
Collaborator Author

Maintainer decision (2026-11-06, /quest-spawn): ✅ leave the pre-fork moq-ffi features moq-c never had (group-by-group track reads, single-group fetch, session request URL and transport, rendition bitrate) to the generated C line after the cut. This PR stays as is.

(Written by Claude Opus 5.5 with high)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Heads-up from the final API audit (moq-dev/moq#8088), since this PR brings moq-c to moq-ffi parity: moq-ffi renames MoqTrackRequest::abort and MoqDatagramRequest::abort to reject, to mirror Rust's track::Request::reject / datagram::Request::reject, JS, and moq-ffi's own MoqBroadcastRequest::reject. It also renames MoqFill::{start_group, start_frame, end_group} to start() -> MoqPosition and end(), to mirror track::Fill. The audit left rs/moq-c alone to avoid racing this PR, so the C mirrors (moq_track_request_abort, moq_datagram_request_abort) would read as moq_track_request_reject / moq_datagram_request_reject for parity, if this PR is the place for it.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

An API note from a review of the open PRs (moq-dev/moq#8214), which scored this PR 6/10 (Mixed):

  • Fix poll_ready-on-finish in moq-net, not in C. moq-c works around group::Producer::poll_ready not waking when the group finishes. If moq-net wakes it on finish (a small separate PR), moq-c can drop the workaround and moq-ffi's matching hang goes away too.
  • Land the agreed renames here: reject, the Fill start()/end() shape, and moq_connection_state.protocol renamed to version, as moq-dev/moq#8088 and its comment asked.
  • Say why fixed/delay_us differ. Add one line to rs/moq-c/README.md explaining that they intentionally invert or relocate moq-ffi's estimate/delay() so that zero-initialized structs are valid. Otherwise this reads as an accidental divergence.

None of this contests the parity decision.

(Written by Claude Opus 5.5 with high)

kixelated and others added 4 commits November 7, 2026 15:58
- Socket endpoints drop finished track producers instead of keeping them until close.
- A grant watch ends with the session's failure reason instead of a clean 0.
- Media consumers enforce a rendition's instance when resolving its broadcast.
- A buffered first dial no longer escapes an accept loop's cancel.
- Document live token replacement without AUTH, the parity scope, and group_ready across a broadcast close.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moq_decode_audio and moq_decode_video now resolve a rendition's broadcast
reference and instance, as the encoded consumers do. The group-ready tests
block instead of timing out, and check parking synchronously.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wait behind moq_publish_group_ready is now its own future, so the tests
poll it directly: each asserts the wait parked, then that finishing the group
or closing the broadcast wakes it, with no real-time sleeps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Automated review by review (OpenAI)

Reviewed a2205c3 incrementally from 4a486d0, separating inherited main changes.

[P2] Let cancellation interrupt decoded sibling resolution — rs/moq-c/src/audio.rs:283–286 and rs/moq-c/src/video.rs:842–845. A rendition can reference a sibling that has not announced yet; Source::resolve deliberately waits for its first announcement. The newly added await runs before Self::run, the only place these decoder tasks poll their close receiver. Calling moq_decode_audio_cancel/moq_decode_video_cancel while that lookup waits therefore leaves the task and callback userdata retained, with no terminal callback until the sibling appears or the origin fails. Task::close only drops the sender, and Background::watch does not observe this cancellation. Race the whole resolution/subscription setup against close, as Consume::until_closed already does for encoded media, and test cancelling a decoder while its sibling lookup is pending in both paths.

The prior socket-writer retention finding is fixed: socket families now drop finished producers while retaining open-child shutdown tracking. The extracted group-ready future also gives the parked-wait tests deterministic wakeup coverage without sleeps. Those are sound, focused changes; preserve that ownership direction.

Verification limits: GitHub-only static review of the substantive follow-up commits and relevant ownership, resolution, cancellation and regression paths. No builds, tests, interop, stress runs or Apple validation performed. Rechecked open/non-draft state, full head and published reviews immediately before this COMMENT.

@pullfrog pullfrog 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.

Important

The new rendition resolution in the decoders can outlive a cancel, leaving the terminal callback and its user_data parked; doc/lib/layers.md also still denies the codec-only C encoders this PR ships.

Reviewed changes

  • Released finished socket tracks: endpoints now use Children::unkept(), so moq_publish_track_finish drops the producer at once while moq_socket_close still ends open halves; a three-round dial/accept/finish test asserts the endpoint keeps nothing.
  • Taught decoded audio and video consumers to resolve a rendition's broadcast reference and instance through the catalog's origin before subscribing, with tests for a followed sibling, a refused foreign instance, and a decoded sibling.
  • Propagated a failed session's end reason from moq_session_grants as a terminal negative code, keeping Cancel/Closed clean, and covered a watcher that outlives its session.
  • Extracted moq_publish_group_ready's wait into api::group_ready; tests now park it by hand across a group finish and a broadcast close, including a declared-end track.
  • Re-raced the socket accept loop's buffered first dial against close, so a cancel wins instead of delivering it.
  • Updated the C token paragraph for live replacement, scoped the m3 C quest to the m1/m2 additions, and dropped the completed cut-gate quest entry.

ℹ️ doc/lib/layers.md still describes C as having no codec-only encoder

doc/lib/layers.md:24 still says the C codecs "always publish to or subscribe from a broadcast, so C has no codec-only Encoder", and the audio/video C rows (:107, :123) list only moq_encode_*/moq_decode_*. This PR ships moq_audio_encoder_* and moq_video_encoder_*, and doc/lib/index.md already names single-group fetch as the only remaining C gap, so the two pages now contradict each other.

Technical details
# layers.md contradicts the codec-only C encoders

## Affected sites
- `doc/lib/layers.md:24` — says C has no codec-only `Encoder`.
- `doc/lib/layers.md:107` — audio C row omits `moq_audio_encoder_*`.
- `doc/lib/layers.md:123` — video C row omits `moq_video_encoder_*`.

## Required outcome
- The exception paragraph and both C rows name the standalone encoder prefixes alongside the broadcast-bound ones, matching `doc/lib/index.md` and what ships.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

Comment thread rs/moq-c/src/audio.rs Resolved
kixelated and others added 4 commits November 7, 2026 21:55
moq_decode_audio/video awaited Rendition::resolve and the decoder's
SUBSCRIBE_OK before polling close, so a cancel while a sibling broadcast
was unannounced parked the task forever. Race the setup against close
through Consume::until_closed, as moq_consume_* already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Automated review by review (OpenAI)

Reviewed e364663 incrementally from a2205c3, separating inherited main changes.

The prior P2 is resolved in both decoded-media paths. Audio and video now race the entire sibling-resolution/subscription setup against close using Consume::until_closed. Cancellation returns cleanly without entering the decode loop or polling the completed oneshot again, allowing the task wrapper to remove its entry and deliver its terminal callback. The added regression checks cancellation for both decoders when the referenced sibling never announces.

No new actionable findings. Direction: reuse this existing setup-cancellation boundary rather than introducing a separate cancellation flag; retain the terminal callback as the userdata lifetime boundary.

Verification limits: GitHub-only static review of the follow-up diff, current setup/helper/task paths and regression; no builds, tests, callback stress or native Apple validation performed. Rechecked open/non-draft state, full head and published reviews immediately before this COMMENT.

@pullfrog pullfrog 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.

ℹ️ No new issues in the decoder-cancel fix. Approval is withheld so the outstanding doc/lib/layers.md note from the prior round is not lost.

Reviewed changes

  • Raced both decoder setups against the task's close: moq_decode_audio and moq_decode_video now run Rendition::resolve and decode::Consumer::new through Consume::until_closed, so a cancel during either wait ends the task with a terminal 0 instead of parking it.
  • Published Consume::until_closed crate-wide and documented that its close receiver must not be polled again once it fires.
  • Added test::decoders_cancel_while_resolving_a_sibling, which points both renditions at a never-announced sibling and asserts each cancel ends cleanly and releases its task.
  • Merged origin/main (conflict-free) and formatted the new test.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

@kixelated
kixelated added this pull request to the merge queue Nov 8, 2026
Merged via the queue into main with commit 889982c Nov 8, 2026
9 of 10 checks passed
@kixelated
kixelated deleted the quest/m2/cut-gate/c/README branch November 8, 2026 07:00
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.

1 participant