Skip to content

feat(obs): the OBS plugin on the generated C++ bindings - #4281

Merged
kixelated merged 12 commits into
quest/m1/cpp/READMEfrom
quest/m1/cpp/obs
Sep 28, 2026
Merged

kixelated merged 12 commits into
quest/m1/cpp/READMEfrom
quest/m1/cpp/obs

Conversation

@kixelated

@kixelated kixelated commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

Problem: cpp/obs reached MoQ through libmoq's int handles and C callbacks on one shared runtime thread, which is why it carried generation counters, heap SessionRef/callback_state trampolines, a condvar refcount with a two-second destroy backstop, and "never take signal_mutex inside a libmoq call" lock-order rules. It also never stopped the runtime before OBS unloads the module. Quest: quest/m1/cpp/obs.md (deleted here).

Approach

  • Build: CMakeLists.txt adds cpp/moq in-tree for MOQ_LOCAL, or fetches the cpp-v<version> release archive and find_package(moq-cpp), and links moq::cpp (renamed in feat(cpp)!: export the C++ package as moq::cpp #4297). The hand-listed Windows system libs are gone: the package carries them. just obs compile/_unit build and install cpp/moq to render the headers.
  • Executor: MoQWorker (src/moq-worker.h), one thread per output and per source, is the executor passed to every then(). No continuation runs on the moq-ffi runtime, a slow one (FFmpeg decode, a frontend handling a signal inline) only delays its own object, and Stop() drops queued tasks and joins, so nothing runs after destroy.
  • Output: moq::Client::connect then a session->status() loop. The attempt counter becomes pointer identity on the owned Attempt, whose drop cancels the pending call; a result already queued on the worker is discarded by that identity check. signal_mutex stays, since OBS itself needs a stop and a failure report serialized; SessionState/SessionRef/Detach are gone. Media goes through BroadcastProducer::publish_video/audio, write_frame, cut, flush under a small media_mutex.
  • Source: one Connection object owns client, session, announced broadcast, catalog, and a Track per rendition; dropping it cancels everything. Refcounts, condvar, backstop timeout, generation and attempt counters are deleted. FFmpeg decode is unchanged.
  • Shutdown: obs_module_unload logs and calls moq::shutdown().
  • Tests: the output and source tests now drive the real moq-ffi over an in-process relay (test/moq-test-relay.h, which also serves the http:// fingerprint). Kept scenarios: stop during connect, failure racing Stop (report window held open), re-entrant Stop from the signal, OBS-driven restart, failures outliving the output (x100), destroy during connect / waiting for an announce / mid-stream. New: real publish into the relay's catalog, reconnect after a relay drop, module unload stopping the runtime (test/obs-moq-test.cpp). TSan needs test/tsan.supp, which suppresses reports with a Rust frame (uninstrumented).
  • Release: the OBS jobs move from libmoq.yml to release-cpp.yml (build.sh --moq-release); obs.yml triggers on cpp/moq/** and rs/moq-ffi/** too. The plugin keeps its own version in cpp/obs/VERSION (0.7.0, replacing buildspec.json's unused 0.0.1), and a C++ release refuses to cut an obs-moq-v<version> that already names another commit. CONTRIBUTING.md gains a # Versions section for moq-cpp and OBS (reconciles with chore: pin shared skills; document package versions in CONTRIBUTING #4272).

Impact

  • No public API or wire change. The plugin only.
  • User-visible, until the new quests land in this line (the maintainer accepted this temporary loss): the Advanced settings drop protocol version, connect timeout, Happy Eyeballs delay, SNI override, QUIC idle timeout, keep-alive, GSO, MTU discovery, congestion control, and qlog (moq-ffi has no setters for them); Stats no longer shows the negotiated draft; the dock no longer names the reason while reconnecting. Tracked by quest/m1/cpp/client-config.md and quest/m1/cpp/session-report.md.

Alternatives

  • OBS threads (graphics/UI via obs_queue_task) as executors, as the quest plan suggested: decode would stall rendering, and there is no "output thread" in OBS. A worker per object gives the same guarantees and a join at destroy.
  • Stubbing the generated uniffi_moq_ffi_* symbols in tests: brittle, and it would no longer exercise the real cancellation and threading.
  • Adding the missing client knobs to moq-ffi in this PR: a sizable API shape decision across every binding, so split into its own quest.

Follow-ups

  • New quests: quest/m1/cpp/client-config.md, quest/m1/cpp/session-report.md (both must land before the line merges).
  • Log and dock text comes from moq::Error::to_string() (feat(cpp): moq::Error prints Rust's message #4292), so the hand-kept mapping is gone.
  • A catalog update still drops the current track and decoder before the replacement subscribes, as the libmoq version did; holding the old track until the new one yields (or skipping an unchanged rendition) is a possible follow-up.
  • AGENTS.md Cross-Package Sync now lists cpp/obs/src and doc/bin/obs.md under rs/moq-ffi.
  • quest/m3/libmoq-shutdown.md (moved to m3 on main) is re-scoped to any host that unloads the C library.
  • Manual, deferred to the cpp line's final review: macOS and Windows builds; a publish/watch round trip against a relay with reconnect and mid-stream source deletion; the exit-crash reproduction (exit with an output running, right after stopping one) with and without this change.

Checks on Linux: just obs compile, just obs ci, just obs test (TSan, 5 runs clean), just obs check, just check.

(Written by Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 3 commits September 26, 2026 12:23
cpp/obs links cpp/moq instead of libmoq. The output and source hold moq::
objects, async work is a future whose continuation runs on a worker thread
each output and source owns, and teardown stops that worker instead of
counting outstanding callbacks. obs_module_unload calls moq::shutdown().

Tests run against the real moq-ffi over an in-process relay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One moq:: namespace for every CMake package: find_package(moq-cpp) links
moq::cpp (was moq-cpp::moq), matching moq::c from find_package(moq-c).
The internal imported staticlib is moq::ffi.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated and others added 2 commits September 26, 2026 14:32
cpp/obs/VERSION (0.7.0) versions the plugin instead of the moq-cpp release it
links, so obs-moq tags don't go backwards, and a C++ release refuses to reuse a
plugin version that already names another commit. CONTRIBUTING.md records both
version sources. libmoq-shutdown is about any host that unloads the C library,
since OBS no longer links it.

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

Copy link
Copy Markdown
Collaborator Author

Decisions

  • Accept the temporary loss of the Advanced settings: the cpp line cannot complete until it reaches parity anyway.
  • The AGENTS.md sync table fix lands in this PR (OBS syncs with moq-ffi, not libmoq).

(Written by Opus 5.5)

kixelated and others added 3 commits September 26, 2026 15:16
# Conflicts:
#	CLAUDE.md
#	cpp/obs/justfile
#	cpp/obs/src/moq-output.cpp
#	cpp/obs/test/moq-output-test.cpp
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review September 26, 2026 22:33
@kixelated

Copy link
Copy Markdown
Collaborator Author

Decisions:

  • Marked ready with the manual checks still open: macOS and Windows just obs build, a publish/watch round trip with reconnect and mid-stream source deletion, and the exit-crash reproduction. They are for the cpp line's final review, since the line cannot complete before settings parity.
  • Links moq::cpp (feat(cpp)!: export the C++ package as moq::cpp #4297).

(Written by Opus 5.5)

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-28T01:49:10.328633Z 19bc290 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cd7b3e5ba8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +696 to +699
moq_source_install_decoder_locked(ctx, std::move(decoder));
conn->video = std::make_shared<Track>("Video", rendition->first, rendition->second.container,
moq_source_decode_frame);
moq_source_subscribe_track(ctx, conn, conn->video, rendition->second.broadcast);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Defer replacing the active track until subscription succeeds

When a catalog update arrives during playback, these lines immediately destroy the working decoder and cancel the current video track before the asynchronous resolve() and subscribe_media() calls succeed. An unavailable sibling broadcast or rejected subscription therefore freezes video until another catalog update, and even successful metadata updates introduce a gap and force another keyframe wait; retain the current track and decoder until moq_source_on_subscribed has a valid replacement. The audio path at lines 721-724 has the same ordering problem.

Useful? React with 👍 / 👎.

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.

Leaving this as is for this PR: it keeps the libmoq version's behavior. That version also installed the new decoder and cancelled the old track as soon as moq_consume_video returned, before any frame from the replacement arrived, and an async subscribe failure left no video either. Holding the old track until the new one yields (or skipping the resubscribe when the rendition didn't change) would be an improvement over today, so it goes to the maintainer as a follow-up rather than growing the migration.

(Written by Claude Opus 5.5)

Comment on lines +119 to +122
obs-build:
name: OBS plugin (${{ matrix.target }})
needs: release
runs-on: ${{ matrix.os }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Run OBS builds in non-tag dry-run workflows

During the nightly workflow_call and manual dry-run, release is skipped by its github.ref_type == 'tag' condition, and GitHub documents that a job needing a skipped job is also skipped unless its condition permits continuation. Consequently this obs-build matrix never tests macOS or Windows packaging before a release tag, despite the workflow's stated dry-run contract; depend on build for non-tag runs or add an explicit tag-aware condition. GitHub Actions documentation.

AGENTS.md reference: AGENTS.md:L23-L23

Useful? React with 👍 / 👎.

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.

Leaving this as is. obs-build passes --moq-release, which fetches the cpp-v<version> archives from the release that release just published, so a dry-run has nothing to link against. It also matches libmoq.yml, which this job moved from. The plugin's compile is gated by obs.yml on PRs and by the nightly on Linux; the macOS and Windows builds are on the manual list for the cpp line's final review.

(Written by Claude Opus 5.5)

Comment thread .github/workflows/release-cpp.yml Outdated
Comment on lines +144 to +147
- name: Parse version
id: parse
shell: bash
run: .github/scripts/release.sh parse-version cpp

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Read the C++ version from its actual version file

When this job runs outside a tag, release.sh parse-version cpp follows its dry-run path and reads rs/cpp/Cargo.toml, which does not exist; the intended nightly/manual OBS package build will therefore fail before compilation. Read cpp/moq/VERSION, as the workflow's existing version check does, or extend the helper to understand the C++ package layout.

AGENTS.md reference: AGENTS.md:L23-L23

Useful? React with 👍 / 👎.

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.

Fixed in acd315f: the job reads cpp/moq/VERSION, which the build job has already checked against the tag. It only runs on a tag today, where parse-version cpp worked, but the file is the source of truth either way.

(Written by Claude Opus 5.5)

}

const auto &raw = connection.stats;
const moq::ConnectionStats raw = current->stats();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Revalidate the session before committing a stats snapshot

If Stop(), a restart, or a disconnect occurs after the live session is copied under mutex but before this assignment, the method returns true with statistics from the retired session and the dock reports the output as connected. The previous implementation rechecked the attempt and session handle before overwriting the caller's snapshot, and its regression test explicitly exercised this race; restore an equivalent identity/live check after stats() returns.

Useful? React with 👍 / 👎.

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.

Agreed, restored in acd315f: after stats() returns, TryGetConnectionStats rechecks under mutex that the session is still the current, live one before handing back the snapshot. No regression test: the window sits inside the synchronous moq-ffi call, and the real-relay tests have no seam to hold it open the way the old stubbed moq_session_snapshot did.

(Written by Claude Opus 5.5)

kixelated and others added 4 commits September 26, 2026 20:45
…rsion file

TryGetConnectionStats rechecks that the session it read is still the live one
before handing back the snapshot, as the libmoq version did. The OBS release
job reads cpp/moq/VERSION instead of release.sh parse-version, whose off-tag
path looks for a Cargo.toml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#4292 gave moq::Error Rust's Display text, so the hand-kept MoQDescribe copy
goes. The failure-code classifier, its one remaining user, moves into
moq-output.cpp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	CONTRIBUTING.md
#	quest/m1/README.md
#	quest/m1/cpp/README.md
#	quest/m1/kt-jvm-exit.md

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 19bc290994

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

while (!done()) {
if (std::chrono::steady_clock::now() > deadline)
return false;
std::this_thread::sleep_for(std::chrono::milliseconds(2));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Replace wall-clock polling with deterministic synchronization

The newly added OBS unit suites use this helper repeatedly to poll asynchronous state with real-time sleeps and a 10-second deadline, while several tests also add fixed sleeps to prove that callbacks did not arrive. Under slower CI or ThreadSanitizer runs, valid behavior can exceed those timing windows and fail nondeterministically, while successful runs incur unnecessary delays. Use fixture signals, condition variables, or mocked time so completion and absence of callbacks are observed deterministically.

AGENTS.md reference: AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

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.

Leaving this as is. These suites run the real moq-ffi over real QUIC against an in-process relay, and nothing in C++ can mock the clock of moq-ffi's tokio runtime. WaitFor waits on a condition with a 10 s ceiling; it doesn't use a sleep to synchronize. A pass takes as long as the event does, and only a run 10 s slower than expected fails (TSan ran clean 5 times). The short fixed sleeps check that a callback did not arrive, so a late callback can only make them pass wrongly, never fail. Swapping polling for fixture signals where the plugin exposes one would be a fine follow-up, but it's out of scope for the migration.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Summary: I merged quest/m1/cpp/README into this branch after #4297 (moq::cpp) landed, which also brings in main with #4321's interop fix. Conflict resolutions:

  • CONTRIBUTING.md: main's version list with this PR's C++ and OBS lines.
  • The quest lists: OBS migration is removed, and client-config and session-report sit ahead of Cancel and C++ standard.
  • libmoq-shutdown now lives at its m3 path.

just obs compile and just obs test pass locally, and CI is green. Every review finding is fixed or answered in its thread. Per the maintainer, the temporary loss of the OBS Advanced settings is accepted, and the manual macOS, Windows, and round-trip checks are deferred to the cpp line's final review.

(Written by Claude Opus 5.5)

@kixelated
kixelated merged commit 2ae9f9a into quest/m1/cpp/README Sep 28, 2026
7 checks passed
@kixelated
kixelated deleted the quest/m1/cpp/obs branch September 28, 2026 02:28
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