Skip to content

feat(obs): vendor the OBS plugin in-tree under cpp/obs#1977

Merged
kixelated merged 4 commits into
mainfrom
claude/festive-jepsen-2f7c1c
Jul 1, 2026
Merged

feat(obs): vendor the OBS plugin in-tree under cpp/obs#1977
kixelated merged 4 commits into
mainfrom
claude/festive-jepsen-2f7c1c

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

Summary

Brings the OBS Studio plugin (github.com/moq-dev/obs, GPL-2.0) into the monorepo as a slim copy under a new cpp/ top-level. The plugin now builds against the in-tree rs/libmoq (no release download) and ships with every libmoq release, instead of living in a separate repo that has to chase a pinned libmoq version.

This re-does the previously-reverted #1787, at explicit request. The earlier revert was over GPL license-scanner noise (see the licensing note below); the tradeoff is being accepted this time.

What changed

  • cpp/obs/ (47 files): vendored the current moq-obs (bb3b128), so it carries the recent upstream fixes (MoQ-source teardown use-after-free fix, portable ffmpeg linkage, Windows cmake install path). Dropped the upstream build-aux/ and .github/ scaffolding.
  • SPDX GPL-2.0-or-later headers on all 10 source files, plus an isolated cpp/obs/LICENSE.
  • MOQ_LOCAL auto-defaults to the repo root (set(... "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH) + EXISTS guard), so CMake builds libmoq from rs/libmoq via cargo with no config. A consumer building cpp/obs in isolation can still override it or fall back to a published release.
  • Base-file wiring: root justfile (mod obs 'cpp/obs'), flake.nix (obsDeps into the dev shell, Linux-only since nixpkgs marks obs-studio broken on Darwin), CLAUDE.md (/cpp/ structure bullet + rs/libmoq C ABI → cpp/obs/src sync row), README.md (GPL exception note), doc/bin/obs.md (rewritten for the in-tree build).
  • Release CI: libmoq.yml gains obs-build + obs-release jobs. On a libmoq-v* tag they rebuild the plugin against the just-published libmoq release (matching ABI, no second cargo build) and cut a parallel obs-moq-v<version> release with macOS arm64 + Windows x64 binaries. Linux stays build-from-source.

Skipped the old PR's rs/libmoq/CMakeLists.txt Windows-libs hunk: that already landed on main via #1792.

Licensing note

libobs is GPL-2.0 with no linking exception, so any distributed OBS plugin must be GPL. It does not infect the rest of the repo: cpp/obs is a separately-distributable work (GPLv2 mere aggregation), and libmoq and the other moq crates stay MIT/Apache for every other consumer. This is scoped with SPDX headers, cpp/obs/LICENSE, and a README exception. The known cost is that full-filesystem license scanners (Black Duck / FOSSA / ScanCode) will report GPL present in the repo; dependency-graph scanners (cargo-deny, npm) won't, since cpp/obs isn't in any package graph.

Test plan

  • Root + cpp/obs justfiles parse; mod obs resolves; flake.nix evaluates on aarch64-darwin
  • actionlint, clang-format, gersemi clean on the vendored tree
  • cargo build -p libmoq green; MOQ_LOCAL resolves to the repo-root rs/libmoq
  • Full macOS / Windows plugin build not re-run in this env (needs to run outside the nix shell; feat(obs): vendor the OBS plugin in-tree under cpp/obs #1787 already verified the macOS + Windows builds against these same files)

Cross-package sync: rs/libmoq C ABI ↔ cpp/obs/src, doc/bin/obs.md (added to the sync table).

(Written by Claude Opus 4.8)

Bring the OBS Studio plugin (github.com/moq-dev/obs, GPL-2.0) into the
monorepo as a slim copy under a new cpp/ top-level, so it builds against
the in-tree rs/libmoq and ships with every libmoq release instead of
tracking a separate repo and libmoq version.

Vendored the current moq-obs tree (carries the source-teardown UAF fix,
portable ffmpeg linkage, and Windows cmake fix), then layered only the
monorepo-specific bits: SPDX GPL-2.0-or-later headers on every source
file, and MOQ_LOCAL defaulting to the repo root so CMake builds libmoq
from rs/libmoq via cargo (no release download). Dropped the upstream
build-aux/ and .github/ scaffolding.

The plugin statically links libmoq, so libmoq.yml gains obs-build and
obs-release jobs: on a libmoq-v* tag it rebuilds the plugin against the
just-published libmoq release and cuts a parallel obs-moq-v<version>
release (macOS arm64 + Windows x64). Linux stays build-from-source.

GPL note: libobs is GPL-2.0 with no linking exception, so any OBS plugin
must be GPL. It does not infect the rest of the repo: cpp/obs is a
separately-distributable work (GPLv2 mere aggregation) and libmoq stays
MIT/Apache for all other consumers. Marked with SPDX headers, an isolated
cpp/obs/LICENSE, and a README exception note.

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

@sourcery-ai sourcery-ai 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.

Sorry @kixelated, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in: 12 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

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.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bdb8c317-1e1f-4fe2-b30f-662f55da6187

📥 Commits

Reviewing files that changed from the base of the PR and between 26540f4 and b751e48.

📒 Files selected for processing (4)
  • .github/workflows/libmoq.yml
  • cpp/obs/build.sh
  • cpp/obs/src/moq-output.cpp
  • cpp/obs/src/moq-source.cpp

Walkthrough

This PR adds a new C++ OBS Studio plugin (obs-moq) located at cpp/obs, which links the in-tree rs/libmoq crate to enable MoQ-based streaming input/output/service integration in OBS. It includes plugin source (output, source, service, optional Qt dock, logging), a full CMake build system with platform-specific configuration for Linux, macOS, and Windows (including packaging, code signing, and dependency fetching), formatting/lint configs, a build.sh packaging script, and a justfile for developer workflows. Documentation (README, CLAUDE.md, doc/bin/obs.md) is updated to describe the plugin's licensing exception (GPL-2.0-or-later) and build/release process. CI workflow changes add jobs to build the plugin for macOS/Windows and publish a separate obs-moq GitHub release alongside libmoq releases. flake.nix and the root justfile are updated to wire in the new module and dependencies.

Changes

Cohort / File(s) Summary
Plugin source (cpp/obs/src/*) New MoQOutput, MoQService, moq_source, logger macros, obs-moq module entrypoint, and optional Qt MoQDock
CMake build system (cpp/obs/cmake/**, CMakeLists.txt, CMakePresets.json) New target definition, FFmpeg discovery, libmoq linkage, per-platform (Linux/macOS/Windows) compiler config, defaults, packaging, and dependency bootstrap
Project config (.clang-format, .gersemirc, .gitignore, buildspec.json) New formatting/lint/build spec configuration for the plugin
Build tooling (build.sh, justfile, root justfile, flake.nix) New release packaging script, developer recipes, and Nix dependency wiring
CI (.github/workflows/libmoq.yml) New obs-build and obs-release jobs to build/publish the plugin
Docs (README.md, CLAUDE.md, cpp/obs/README.md, cpp/obs/LICENSE, doc/bin/obs.md) Licensing exception notes and build/release documentation

Sequence Diagram(s)

sequenceDiagram
  participant releaseJob as release
  participant obsBuild as obs-build
  participant buildScript as build.sh
  participant obsRelease as obs-release
  releaseJob->>obsBuild: triggers after libmoq release
  obsBuild->>buildScript: parse-version libmoq, run build.sh --libmoq-release
  buildScript-->>obsBuild: obs-moq artifact per target
  obsBuild->>obsRelease: upload obs-moq-${matrix.target}
  obsRelease->>obsRelease: download obs-moq-* artifacts
  obsRelease->>obsRelease: create obs-moq-v<version> release
Loading

Related PRs: None identified.

Suggested labels: cpp, obs-plugin, build-system, ci

Suggested reviewers: None identified.

Poem
A rabbit hops through OBS halls,
Linking libmoq where the stream call falls,
CMake presets stacked so neat,
Mac, Windows, Linux — each a treat,
Hop, build, release — the plugin's complete! 🐇📦

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: vendoring the OBS plugin under cpp/obs.
Description check ✅ Passed The description is detailed and directly describes the vendored OBS plugin, build, licensing, and release workflow changes.
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.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/festive-jepsen-2f7c1c

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.

just ci runs shfmt/shellcheck over every shell script, and the repo's
.editorconfig pins *.sh to 4-space indent. The vendored build.sh used
upstream's tab style, failing the format check. Reindent it to the house
style (pure whitespace, no logic change); a future upstream re-vendor
just re-runs `just fix` to regenerate this deterministically.

Also drop fetch-depth: 0 from the obs-build CI job: it only compiles and
never touches git history or tags, so a shallow checkout is enough.

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

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

🧹 Nitpick comments (4)
cpp/obs/cmake/common/bootstrap.cmake (1)

34-41: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Dead code after FATAL_ERROR.

message(FATAL_ERROR ...) halts configuration immediately, so the file(REMOVE_RECURSE ...) cleanup on Line 40 never executes. This is unreachable and appears to be inherited boilerplate; if the intent was to clean up an accidental in-source CMakeCache.txt/CMakeFiles before erroring, the order needs to be swapped.

Suggested fix
 if("${CMAKE_CURRENT_BINARY_DIR}" STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}")
+  file(REMOVE_RECURSE "${CMAKE_CURRENT_SOURCE_DIR}/CMakeCache.txt" "${CMAKE_CURRENT_SOURCE_DIR}/CMakeFiles")
   message(
     FATAL_ERROR
     "In-source builds are not supported. "
     "Specify a build directory via 'cmake -S <SOURCE DIRECTORY> -B <BUILD_DIRECTORY>' instead."
   )
-  file(REMOVE_RECURSE "${CMAKE_CURRENT_SOURCE_DIR}/CMakeCache.txt" "${CMAKE_CURRENT_SOURCE_DIR}/CMakeFiles")
 endif()
🤖 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 `@cpp/obs/cmake/common/bootstrap.cmake` around lines 34 - 41, The in-source
build guard in bootstrap.cmake has unreachable cleanup because
`message(FATAL_ERROR ...)` stops execution before `file(REMOVE_RECURSE ...)` can
run. Update the `if("${CMAKE_CURRENT_BINARY_DIR}" STREQUAL
"${CMAKE_CURRENT_SOURCE_DIR}")` block so any intended cleanup of
`CMakeCache.txt` and `CMakeFiles` happens before the fatal error, or remove the
dead cleanup entirely if it is no longer needed.
cpp/obs/CMakeLists.txt (2)

16-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated FFmpeg linking logic between BUILD_PLUGIN branches.

Lines 24-25 and 34-35 both call find_package(FFmpeg REQUIRED avcodec avutil swscale swresample) and link the same four FFmpeg::* targets. Consider extracting this into a small helper/macro to avoid drift if the required FFmpeg components change later.

🤖 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 `@cpp/obs/CMakeLists.txt` around lines 16 - 36, The FFmpeg setup in the
BUILD_PLUGIN conditional duplicates the same find_package and
target_link_libraries logic in both branches, which can drift over time.
Refactor the repeated FFmpeg package discovery and linking for obs-moq into a
small helper function or CMake macro, and call it from both the BUILD_PLUGIN and
non-plugin paths so the required components and linked FFmpeg::* targets stay
centralized and consistent.

112-114: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

find_package(Qt6 ...) isn't marked REQUIRED.

When ENABLE_QT is on but Qt6 isn't found, find_package will silently succeed (returning not-found), and the subsequent target_link_libraries(obs-moq PRIVATE Qt6::Core Qt6::Widgets) will fail with a less-clear "target not found" error rather than a clear Qt6-not-found message.

Suggested fix
-  find_package(Qt6 COMPONENTS Widgets Core)
+  find_package(Qt6 REQUIRED COMPONENTS Widgets Core)
🤖 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 `@cpp/obs/CMakeLists.txt` around lines 112 - 114, The Qt6 dependency check in
the ENABLE_QT block is too weak because `find_package(Qt6 COMPONENTS Widgets
Core)` is not REQUIRED, so failures surface later in
`target_link_libraries(obs-moq PRIVATE Qt6::Core Qt6::Widgets)` with a confusing
missing-target error. Update the `find_package` call in the CMakeLists Qt setup
to require Qt6 explicitly, so configuration stops early with a clear
Qt6-not-found failure.
cpp/obs/cmake/windows/helpers.cmake (1)

57-58: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Resource file attached to CMAKE_PROJECT_NAME, not the passed target param.

set_target_properties_plugin(target) attaches the generated .rc to ${CMAKE_PROJECT_NAME} rather than ${target}. This is correct only if the function is always called with the top-level project's plugin target (the common single-target case, matching upstream obs-plugintemplate). If this helper is ever invoked for an additional/secondary target, the .rc would be misattached.

🤖 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 `@cpp/obs/cmake/windows/helpers.cmake` around lines 57 - 58, The generated
resource file is being attached to CMAKE_PROJECT_NAME instead of the function’s
target parameter in set_target_properties_plugin. Update the helper to use the
passed target symbol for the target_sources attachment (and any related property
setup) so the .rc follows the actual target being configured, not only the
top-level project target.
🤖 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 @.github/workflows/libmoq.yml:
- Around line 192-199: The “Find previous obs-moq tag” step fails on the first
release because the pipeline exits when no prior tag matches, so update the tag
lookup to tolerate an empty result in the workflow step using the prev_tag id
and its shell run block. Make the search for the previous obs-moq-v* tag
non-fatal when grep finds nothing, and always write a tag value to GITHUB_OUTPUT
even if it is empty so the first release can proceed.

In `@cpp/obs/build.sh`:
- Around line 63-96: Normalize the auto-detected TARGET in build.sh before the
case statement so macOS toolchains don’t hit the unsupported branch; cc
-dumpmachine returns version-suffixed Darwin triples, so add canonicalization
for macOS to match the existing macos preset path. Update the TARGET handling
near the cc -dumpmachine default and the later case on TARGET so both local
./build.sh and --libmoq-release use the canonical apple-darwin form expected by
CMakePresets.json and CMakeLists.txt.

In `@cpp/obs/cmake/common/buildspec_common.cmake`:
- Around line 178-187: The failed-download cleanup in the buildspec download
block is unreachable because message(FATAL_ERROR) aborts before file(REMOVE)
runs. Update the logic in the download handling around file(DOWNLOAD) so the
partial archive is removed before calling message(FATAL_ERROR), or switch to a
temporary download path in buildspec_common.cmake and only rename it on success.
Use the existing download_status, error_code, and error_message flow to preserve
the current error handling while ensuring stale files are cleaned up.
- Around line 55-72: The OBS configure step is building several CMake flags as
single strings, which makes execute_process(COMMAND ...) pass invalid argv
entries and literal quotes. Update the argument construction in
buildspec_common.cmake around execute_process, _cmake_arch, and _cmake_extra so
each flag/value is its own command argument instead of concatenated text. Split
the Windows -A option, the -D options, and the macOS
architecture/deployment-target values into separate list elements, and avoid
embedding shell-style quotes in the arguments.

In `@cpp/obs/cmake/common/FindFFmpeg.cmake`:
- Around line 131-142: The FFmpeg finder is still calling pkg-config for all
non-Windows hosts, which lets macOS pick up Homebrew/system FFmpeg hints instead
of the obs-deps copy. Update the logic in FindFFmpeg.cmake around the
pkg_search_module() path to skip pkg-config on Apple builds, or restrict it to
Linux/BSD only, so FFmpeg_${component}_INCLUDE_DIR and
FFmpeg_${component}_LIBRARY are resolved only from the intended obs-deps
locations. Keep the change scoped to the existing pkg-config and
find_path/find_library flow so the macOS routing from cpp/obs/CMakeLists.txt
remains intact.

In `@cpp/obs/cmake/linux/compilerconfig.cmake`:
- Around line 53-63: The GNU-specific warning toggles in compilerconfig.cmake
are using the C compiler version instead of the C++ compiler version, so the
checks under the CMAKE_CXX_COMPILER_ID STREQUAL GNU branch should be switched to
CMAKE_CXX_COMPILER_VERSION. Update both version comparisons in the GNU C++
configuration block so the add_compile_options calls for maybe-uninitialized,
infinite-recursion, and stringop-overflow are driven by the actual C++ compiler
version.

In `@cpp/obs/CMakePresets.json`:
- Around line 22-58: The macOS presets in CMakePresets.json are mislabeled as
universal builds while `CMAKE_OSX_ARCHITECTURES` in the `macos` preset is set to
arm64 only. Update the `macos` preset (and therefore `macos-ci`, which inherits
it) to build both architectures with `CMAKE_OSX_ARCHITECTURES`, or else rename
the `macos` and `macos-ci` preset names/descriptions to explicitly reflect
arm64-only behavior. Use the `macos` preset as the source of truth so the
inherited CI preset stays consistent.

In `@cpp/obs/src/moq-output.cpp`:
- Around line 31-37: The teardown in MoQOutput still allows a use-after-free
because the wait_for timeout can release this before the terminal session
callback has drained. Update MoQOutput::Stop/teardown and the session callback
path so the callback state is owned independently of MoQOutput, or so
destruction blocks until the session is fully detached and outstanding_sessions
reaches zero without timing out. Use the existing session_mutex, session_cv, and
outstanding_sessions symbols to keep the callback-safe state alive until libmoq
is completely finished with it.
- Around line 121-138: The publish failure path in moq-output.cpp leaves the
connected MoQ session alive after moq_session_connect() succeeds, which can
stale the session handle for future retries. In the broadcast publish logic
around session_connect_callback and moq_origin_publish, make sure to close the
active session before returning false on publish failure, either by calling
moq_session_close(session) or by routing through Stop(false), so the terminal
callback can clear the outstanding-session reference.
- Around line 91-108: The terminal path in session_connect_callback only
releases bookkeeping, but it should also stop the output when code <= 0 so OBS
does not keep running after an unexpected close. Update MoQOutput::Stop() and
the session callback in moq-output.cpp so terminal callbacks route through
obs_output_signal_stop() (or Stop()) and ensure the user-initiated Stop() path
does not emit the stop signal twice.

In `@cpp/obs/src/moq-source.cpp`:
- Around line 284-285: The reconnect log in moq-source.cpp is exposing the raw
relay URL via LOG_INFO in the settings-changed path, which can leak credentials
or tokens. Update the logging in the relevant reconnect handler to use a
redacted URL representation instead of url, preserving only safe host/path
information while keeping the broadcast value as-is. Use the existing
reconnect/logging code around the settings-changed message in moq-source::
settings update flow and coordinate the same fix upstream for the vendored
cpp/obs change.
- Around line 225-251: The teardown path in moq_source_disconnect_locked
currently logs a timeout but still destroys synchronization primitives and frees
ctx even when ctx->refs remains nonzero, which can leave libmoq callbacks using
freed state. Update the ctx reference handling so that when the wait in the
teardown block times out, the function does not call pthread_cond_destroy(),
pthread_mutex_destroy(), or bfree(ctx); instead, keep ctx alive until the final
terminal callback releases the last reference, or intentionally leak-and-log on
timeout. Preserve the existing refs_zero/refs mutex flow and only free ctx after
refs reaches zero.
- Around line 1118-1120: The frame timestamp assignment in moq-source.cpp is
using frame_data.timestamp_us directly in ctx->frame.timestamp, but that field
expects nanoseconds. Update the timestamp conversion in the frame output path
before obs_source_output_video so the value is normalized from microseconds to
nanoseconds (for example, by converting in the ctx->frame.timestamp assignment),
keeping the change localized around the frame_data / ctx->frame handling.
- Around line 961-1018: The packet decode path in moq-source.cpp drops delayed
output for codecs with reordering because avcodec_receive_frame() is only called
once and avcodec_send_packet() EAGAIN is treated as a failure. Update the decode
flow around the send/receive logic in the packet handling function to keep
draining avcodec_receive_frame() in a loop until EAGAIN, and if
avcodec_send_packet() returns EAGAIN, drain existing frames first and then
resend the same packet instead of discarding it. Keep the error handling and
consecutive_decode_errors tracking in that same decode path aligned with the new
retry behavior.

In `@cpp/obs/src/obs-moq.cpp`:
- Around line 1-18: Remove the template copyright boilerplate from the header in
obs-moq.cpp; the literal placeholders in the file comment are inaccurate. Update
the top-of-file comment to keep only the SPDX identifier or replace the
placeholder block with the real copyright notice, matching the style used
elsewhere in the vendored OBS code.

---

Nitpick comments:
In `@cpp/obs/cmake/common/bootstrap.cmake`:
- Around line 34-41: The in-source build guard in bootstrap.cmake has
unreachable cleanup because `message(FATAL_ERROR ...)` stops execution before
`file(REMOVE_RECURSE ...)` can run. Update the `if("${CMAKE_CURRENT_BINARY_DIR}"
STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}")` block so any intended cleanup of
`CMakeCache.txt` and `CMakeFiles` happens before the fatal error, or remove the
dead cleanup entirely if it is no longer needed.

In `@cpp/obs/cmake/windows/helpers.cmake`:
- Around line 57-58: The generated resource file is being attached to
CMAKE_PROJECT_NAME instead of the function’s target parameter in
set_target_properties_plugin. Update the helper to use the passed target symbol
for the target_sources attachment (and any related property setup) so the .rc
follows the actual target being configured, not only the top-level project
target.

In `@cpp/obs/CMakeLists.txt`:
- Around line 16-36: The FFmpeg setup in the BUILD_PLUGIN conditional duplicates
the same find_package and target_link_libraries logic in both branches, which
can drift over time. Refactor the repeated FFmpeg package discovery and linking
for obs-moq into a small helper function or CMake macro, and call it from both
the BUILD_PLUGIN and non-plugin paths so the required components and linked
FFmpeg::* targets stay centralized and consistent.
- Around line 112-114: The Qt6 dependency check in the ENABLE_QT block is too
weak because `find_package(Qt6 COMPONENTS Widgets Core)` is not REQUIRED, so
failures surface later in `target_link_libraries(obs-moq PRIVATE Qt6::Core
Qt6::Widgets)` with a confusing missing-target error. Update the `find_package`
call in the CMakeLists Qt setup to require Qt6 explicitly, so configuration
stops early with a clear Qt6-not-found failure.
🪄 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: bfe263f9-6cbc-4c0b-8421-03949c032fe8

📥 Commits

Reviewing files that changed from the base of the PR and between e334822 and 26540f4.

📒 Files selected for processing (53)
  • .github/workflows/libmoq.yml
  • CLAUDE.md
  • README.md
  • cpp/obs/.clang-format
  • cpp/obs/.gersemirc
  • cpp/obs/.gitignore
  • cpp/obs/CMakeLists.txt
  • cpp/obs/CMakePresets.json
  • cpp/obs/LICENSE
  • cpp/obs/README.md
  • cpp/obs/build.sh
  • cpp/obs/buildspec.json
  • cpp/obs/cmake/common/FindFFmpeg.cmake
  • cpp/obs/cmake/common/bootstrap.cmake
  • cpp/obs/cmake/common/buildnumber.cmake
  • cpp/obs/cmake/common/buildspec_common.cmake
  • cpp/obs/cmake/common/ccache.cmake
  • cpp/obs/cmake/common/compiler_common.cmake
  • cpp/obs/cmake/common/helpers_common.cmake
  • cpp/obs/cmake/common/osconfig.cmake
  • cpp/obs/cmake/linux/compilerconfig.cmake
  • cpp/obs/cmake/linux/defaults.cmake
  • cpp/obs/cmake/linux/helpers.cmake
  • cpp/obs/cmake/macos/buildspec.cmake
  • cpp/obs/cmake/macos/compilerconfig.cmake
  • cpp/obs/cmake/macos/defaults.cmake
  • cpp/obs/cmake/macos/helpers.cmake
  • cpp/obs/cmake/macos/resources/ccache-launcher-c.in
  • cpp/obs/cmake/macos/resources/ccache-launcher-cxx.in
  • cpp/obs/cmake/macos/resources/create-package.cmake.in
  • cpp/obs/cmake/macos/resources/distribution.in
  • cpp/obs/cmake/macos/resources/installer-macos.pkgproj.in
  • cpp/obs/cmake/macos/xcode.cmake
  • cpp/obs/cmake/windows/buildspec.cmake
  • cpp/obs/cmake/windows/compilerconfig.cmake
  • cpp/obs/cmake/windows/defaults.cmake
  • cpp/obs/cmake/windows/helpers.cmake
  • cpp/obs/cmake/windows/resources/resource.rc.in
  • cpp/obs/data/locale/en-US.ini
  • cpp/obs/justfile
  • cpp/obs/src/logger.h
  • cpp/obs/src/moq-dock.cpp
  • cpp/obs/src/moq-dock.h
  • cpp/obs/src/moq-output.cpp
  • cpp/obs/src/moq-output.h
  • cpp/obs/src/moq-service.cpp
  • cpp/obs/src/moq-service.h
  • cpp/obs/src/moq-source.cpp
  • cpp/obs/src/moq-source.h
  • cpp/obs/src/obs-moq.cpp
  • doc/bin/obs.md
  • flake.nix
  • justfile

Comment thread .github/workflows/libmoq.yml
Comment thread cpp/obs/build.sh
Comment on lines +63 to +96
if [[ -z "$TARGET" ]]; then
TARGET=$(cc -dumpmachine 2>/dev/null || echo unknown)
echo "Detected target: $TARGET"
fi

# Default the version from buildspec.json's top-level "version" (the nested
# dependency entries also have "version" keys, hence the leading-indent anchor).
if [[ -z "$VERSION" ]]; then
VERSION=$(grep -E '^[[:space:]]{4}"version"' "$SCRIPT_DIR/buildspec.json" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
echo "Detected version: $VERSION"
fi

# Map the target triple to a CMake preset and build tree.
case "$TARGET" in
*-linux-*)
PRESET="ubuntu-x86_64"
BUILD_DIR="$SCRIPT_DIR/build_x86_64"
KIND="unix"
;;
*-apple-darwin)
PRESET="macos"
BUILD_DIR="$SCRIPT_DIR/build_macos"
KIND="macos"
;;
*-windows-*)
PRESET="windows-x64"
BUILD_DIR="$SCRIPT_DIR/build_x64"
KIND="windows"
;;
*)
echo "Unsupported target: $TARGET" >&2
exit 1
;;
esac

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Normalize the auto-detected macOS triple before using it.

The default detection path feeds cc -dumpmachine straight into this case statement, but the macOS branch only matches *-apple-darwin. Apple toolchains report version-suffixed Darwin triples, so local ./build.sh falls into Unsupported target, and --libmoq-release would also forward a non-canonical MOQ_TARGET that does not match the release archive naming used by cpp/obs/CMakePresets.json / cpp/obs/CMakeLists.txt.

💡 Suggested fix
 if [[ -z "$TARGET" ]]; then
 	TARGET=$(cc -dumpmachine 2>/dev/null || echo unknown)
+	case "$TARGET" in
+	arm64-apple-darwin*) TARGET="aarch64-apple-darwin" ;;
+	x86_64-apple-darwin*) TARGET="x86_64-apple-darwin" ;;
+	esac
 	echo "Detected target: $TARGET"
 fi
@@
-*-apple-darwin)
+*-apple-darwin*)
 	PRESET="macos"
 	BUILD_DIR="$SCRIPT_DIR/build_macos"
 	KIND="macos"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if [[ -z "$TARGET" ]]; then
TARGET=$(cc -dumpmachine 2>/dev/null || echo unknown)
echo "Detected target: $TARGET"
fi
# Default the version from buildspec.json's top-level "version" (the nested
# dependency entries also have "version" keys, hence the leading-indent anchor).
if [[ -z "$VERSION" ]]; then
VERSION=$(grep -E '^[[:space:]]{4}"version"' "$SCRIPT_DIR/buildspec.json" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
echo "Detected version: $VERSION"
fi
# Map the target triple to a CMake preset and build tree.
case "$TARGET" in
*-linux-*)
PRESET="ubuntu-x86_64"
BUILD_DIR="$SCRIPT_DIR/build_x86_64"
KIND="unix"
;;
*-apple-darwin)
PRESET="macos"
BUILD_DIR="$SCRIPT_DIR/build_macos"
KIND="macos"
;;
*-windows-*)
PRESET="windows-x64"
BUILD_DIR="$SCRIPT_DIR/build_x64"
KIND="windows"
;;
*)
echo "Unsupported target: $TARGET" >&2
exit 1
;;
esac
if [[ -z "$TARGET" ]]; then
TARGET=$(cc -dumpmachine 2>/dev/null || echo unknown)
case "$TARGET" in
arm64-apple-darwin*) TARGET="aarch64-apple-darwin" ;;
x86_64-apple-darwin*) TARGET="x86_64-apple-darwin" ;;
esac
echo "Detected target: $TARGET"
fi
# Default the version from buildspec.json's top-level "version" (the nested
# dependency entries also have "version" keys, hence the leading-indent anchor).
if [[ -z "$VERSION" ]]; then
VERSION=$(grep -E '^[[:space:]]{4}"version"' "$SCRIPT_DIR/buildspec.json" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
echo "Detected version: $VERSION"
fi
# Map the target triple to a CMake preset and build tree.
case "$TARGET" in
*-linux-*)
PRESET="ubuntu-x86_64"
BUILD_DIR="$SCRIPT_DIR/build_x86_64"
KIND="unix"
;;
*-apple-darwin*)
PRESET="macos"
BUILD_DIR="$SCRIPT_DIR/build_macos"
KIND="macos"
;;
*-windows-*)
PRESET="windows-x64"
BUILD_DIR="$SCRIPT_DIR/build_x64"
KIND="windows"
;;
*)
echo "Unsupported target: $TARGET" >&2
exit 1
;;
esac
🤖 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 `@cpp/obs/build.sh` around lines 63 - 96, Normalize the auto-detected TARGET in
build.sh before the case statement so macOS toolchains don’t hit the unsupported
branch; cc -dumpmachine returns version-suffixed Darwin triples, so add
canonicalization for macOS to match the existing macos preset path. Update the
TARGET handling near the cc -dumpmachine default and the later case on TARGET so
both local ./build.sh and --libmoq-release use the canonical apple-darwin form
expected by CMakePresets.json and CMakeLists.txt.

Comment on lines +55 to +72
if(OS_WINDOWS)
set(_cmake_generator "${CMAKE_GENERATOR}")
set(_cmake_arch "-A ${arch},version=${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}")
set(_cmake_extra "-DCMAKE_SYSTEM_VERSION=${CMAKE_SYSTEM_VERSION} -DCMAKE_ENABLE_SCRIPTING=OFF")
elseif(OS_MACOS)
set(_cmake_generator "Xcode")
set(_cmake_arch "-DCMAKE_OSX_ARCHITECTURES:STRING='arm64;x86_64'")
set(_cmake_extra "-DCMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}")
endif()

message(STATUS "Configure ${label} (${arch})")
execute_process(
COMMAND
"${CMAKE_COMMAND}" -S "${dependencies_dir}/${_obs_destination}" -B
"${dependencies_dir}/${_obs_destination}/build_${arch}" -G ${_cmake_generator} "${_cmake_arch}"
-DOBS_CMAKE_VERSION:STRING=3.0.0 -DENABLE_PLUGINS:BOOL=OFF -DENABLE_FRONTEND:BOOL=OFF
-DOBS_VERSION_OVERRIDE:STRING=${_obs_version} "-DCMAKE_PREFIX_PATH='${CMAKE_PREFIX_PATH}'" ${_is_fresh}
${_cmake_extra}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="cpp/obs/cmake/common/buildspec_common.cmake"

echo "== outline =="
ast-grep outline "$file" --view expanded || true

echo
echo "== relevant line ranges =="
sed -n '45,90p' "$file"
echo
sed -n '165,205p' "$file"

echo
echo "== search for execute_process and file(REMOVE) =="
rg -n 'execute_process|file\(REMOVE|_cmake_arch|_cmake_extra|CMAKE_PREFIX_PATH|CMAKE_OSX_ARCHITECTURES|CMAKE_ENABLE_SCRIPTING|CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION' "$file"

Repository: moq-dev/moq

Length of output: 4943


🌐 Web query:

CMake execute_process COMMAND list arguments are not shell-split message(FATAL_ERROR) stops processing file(DOWNLOAD partial file on failure

💡 Result:

The behaviors you described involve how CMake processes commands and errors: 1. execute_process and shell-splitting: CMake's execute_process command does not use a shell to interpret the COMMAND arguments [1][2]. Instead, it passes the arguments directly to the operating system API as an array of strings (argv-style) on POSIX platforms [1][3]. On Windows, it handles command-line encoding to ensure child processes correctly receive the intended arguments [1][4]. Because no shell is invoked, features like shell operators (e.g., >, |, &&) will not work as expected and are treated as literal arguments to the command [1][4]. To run multiple commands in sequence or use pipes, you should use multiple execute_process calls or explicitly invoke a shell (e.g., "bash", "-c", "your command string") [1][5]. 2. message(FATAL_ERROR) behavior: The message(FATAL_ERROR...) command stops both the processing of the current CMake file and the entire configuration generation process [6][7]. When encountered, CMake exits with a non-zero return code, preventing further script execution or build system generation [6][8]. 3. file(DOWNLOAD) failure: By default, the file(DOWNLOAD...) command treats failure as a fatal error that stops CMake processing [9][10]. Even if you provide a STATUS variable, the command will still terminate if the download fails (for instance, due to a network issue or hash mismatch), leaving no opportunity to check the status variable in the script [10]. CMake does not currently provide a native option to "continue on error" for file(DOWNLOAD) to allow custom error handling within the same script [11][10]. To manage this, developers often use alternative strategies, such as checking for file existence beforehand or wrapping download logic in external scripts [12][10].

Citations:


Split the CMake flags into separate arguments. execute_process(COMMAND ...) passes argv literally, so packing -A, the two -D options, and the quoted macOS values into single strings makes the OBS configure step receive invalid options or literal quotes.

🤖 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 `@cpp/obs/cmake/common/buildspec_common.cmake` around lines 55 - 72, The OBS
configure step is building several CMake flags as single strings, which makes
execute_process(COMMAND ...) pass invalid argv entries and literal quotes.
Update the argument construction in buildspec_common.cmake around
execute_process, _cmake_arch, and _cmake_extra so each flag/value is its own
command argument instead of concatenated text. Split the Windows -A option, the
-D options, and the macOS architecture/deployment-target values into separate
list elements, and avoid embedding shell-style quotes in the arguments.

Comment on lines +178 to +187
if(NOT EXISTS "${dependencies_dir}/${file}")
message(STATUS "Downloading ${url}")
file(DOWNLOAD "${url}" "${dependencies_dir}/${file}" STATUS download_status EXPECTED_HASH SHA256=${hash})

list(GET download_status 0 error_code)
list(GET download_status 1 error_message)
if(error_code GREATER 0)
message(STATUS "Downloading ${url} - Failure")
message(FATAL_ERROR "Unable to download ${url}, failed with error: ${error_message}")
file(REMOVE "${dependencies_dir}/${file}")

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '170,195p' cpp/obs/cmake/common/buildspec_common.cmake

Repository: moq-dev/moq

Length of output: 1064


🌐 Web query:

CMake file(DOWNLOAD) failed transfer partial file left behind EXPECTED_HASH message(FATAL_ERROR immediate stop file(REMOVE) unreachable

💡 Result:

The behavior you are describing is a known limitation in how CMake handles file downloads during the configuration stage [1]. When file(DOWNLOAD ...) is used with EXPECTED_HASH, or when it otherwise fails, and you have logic that triggers message(FATAL_ERROR ...), CMake immediately halts execution [1]. Because message(FATAL_ERROR) stops the configuration process, any subsequent commands—including file(REMOVE ...) intended to clean up a partial or corrupted file—are never executed [1]. This leaves the partial or incorrect file on your filesystem. If you run CMake again, the file(DOWNLOAD) command may detect that the file already exists and, depending on the circumstances, potentially skip a re-download or fail again, leaving you stuck with the corrupted local state [2][1]. To solve this issue, it is recommended to adopt one of the following patterns: 1. Atomic Renaming (Recommended): Download the file to a temporary location (e.g., <file>.tmp) first. Only after the file(DOWNLOAD ...) command succeeds (and the hash is verified) should you move or rename the temporary file to the final destination [1]. This ensures that if the download fails or the process is interrupted, your final destination file is never created or overwritten by a partial/corrupted download [1]. 2. Use STATUS to Handle Failures: Instead of letting file(DOWNLOAD) cause an implicit failure, use the STATUS argument to capture the result code [3]. This allows you to check if the download succeeded before proceeding, giving you an opportunity to manually remove any failed files using file(REMOVE ...) before calling message(FATAL_ERROR ...) [3][1]. Example of the STATUS pattern: file(DOWNLOAD STATUS download_status) list(GET download_status 0 status_code) if(NOT status_code EQUAL 0) file(REMOVE ) message(FATAL_ERROR "Download failed: ${download_status}") endif This approach ensures that you retain control over the filesystem state regardless of whether the network request succeeds or fails [3][1].

Citations:


Delete the failed archive before aborting. message(FATAL_ERROR ...) stops immediately, so file(REMOVE ...) never runs; a failed download can leave a stale archive behind and block the next configure. Move the removal before the fatal error, or download to a temp path and rename on success.

🤖 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 `@cpp/obs/cmake/common/buildspec_common.cmake` around lines 178 - 187, The
failed-download cleanup in the buildspec download block is unreachable because
message(FATAL_ERROR) aborts before file(REMOVE) runs. Update the logic in the
download handling around file(DOWNLOAD) so the partial archive is removed before
calling message(FATAL_ERROR), or switch to a temporary download path in
buildspec_common.cmake and only rename it on success. Use the existing
download_status, error_code, and error_message flow to preserve the current
error handling while ensuring stale files are cleaned up.

Comment on lines +131 to +142
if(NOT CMAKE_HOST_SYSTEM_NAME MATCHES "Windows")
find_package(PkgConfig QUIET)
if(PKG_CONFIG_FOUND)
pkg_search_module(PC_FFmpeg_${component} QUIET ${component_libname})
endif()
endif()

find_path(
FFmpeg_${component}_INCLUDE_DIR
NAMES ${component_libname}/${component_header} ${component_libname}/version.h
HINTS ${PC_FFmpeg_${component}_INCLUDE_DIRS}
PATHS /usr/include /usr/local/include

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Skip pkg-config on macOS in this finder.

cpp/obs/CMakeLists.txt routes Apple builds through this module so it binds the FFmpeg shipped in obs-deps. Querying pkg_search_module() on every non-Windows host feeds Homebrew/system include and library hints back into find_path() and find_library(), so a macOS machine with another FFmpeg installed can silently link against the wrong ABI. Limit the pkg-config path to Linux/BSD, or make it an explicit opt-in.

Suggested fix
-  if(NOT CMAKE_HOST_SYSTEM_NAME MATCHES "Windows")
+  if(CMAKE_SYSTEM_NAME MATCHES "Linux|FreeBSD|OpenBSD")
     find_package(PkgConfig QUIET)
     if(PKG_CONFIG_FOUND)
       pkg_search_module(PC_FFmpeg_${component} QUIET ${component_libname})
     endif()
   endif()

Also applies to: 157-161

🤖 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 `@cpp/obs/cmake/common/FindFFmpeg.cmake` around lines 131 - 142, The FFmpeg
finder is still calling pkg-config for all non-Windows hosts, which lets macOS
pick up Homebrew/system FFmpeg hints instead of the obs-deps copy. Update the
logic in FindFFmpeg.cmake around the pkg_search_module() path to skip pkg-config
on Apple builds, or restrict it to Linux/BSD only, so
FFmpeg_${component}_INCLUDE_DIR and FFmpeg_${component}_LIBRARY are resolved
only from the intended obs-deps locations. Keep the change scoped to the
existing pkg-config and find_path/find_library flow so the macOS routing from
cpp/obs/CMakeLists.txt remains intact.

Comment thread cpp/obs/src/moq-source.cpp
Comment thread cpp/obs/src/moq-source.cpp Outdated
Comment on lines +961 to +1018
// Send packet to decoder
int ret = avcodec_send_packet(ctx->codec_ctx, packet);
av_packet_free(&packet);

if (ret < 0) {
if (ret != AVERROR(EAGAIN)) {
ctx->consecutive_decode_errors++;
char errbuf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(ret, errbuf, sizeof(errbuf));

// If too many consecutive errors, flush decoder and wait for next keyframe
if (ctx->consecutive_decode_errors >= 5) {
LOG_WARNING("Too many send errors (%u), flushing decoder and waiting for keyframe",
ctx->consecutive_decode_errors);
avcodec_flush_buffers(ctx->codec_ctx);
ctx->got_keyframe = false;
ctx->consecutive_decode_errors = 0;
} else if (ctx->consecutive_decode_errors == 1) {
LOG_ERROR("Error sending packet to decoder: %s", errbuf);
}
}
pthread_mutex_unlock(&ctx->mutex);
moq_consume_frame_close(frame_id);
return;
}

// Receive decoded frames
AVFrame *frame = av_frame_alloc();
if (!frame) {
pthread_mutex_unlock(&ctx->mutex);
moq_consume_frame_close(frame_id);
return;
}

ret = avcodec_receive_frame(ctx->codec_ctx, frame);
if (ret < 0) {
if (ret != AVERROR(EAGAIN)) {
ctx->consecutive_decode_errors++;
char errbuf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(ret, errbuf, sizeof(errbuf));

// If too many consecutive errors, flush decoder and wait for next keyframe
if (ctx->consecutive_decode_errors >= 5) {
LOG_WARNING("Too many decode errors (%u), flushing decoder and waiting for keyframe",
ctx->consecutive_decode_errors);
avcodec_flush_buffers(ctx->codec_ctx);
ctx->got_keyframe = false;
ctx->consecutive_decode_errors = 0;
} else if (ctx->consecutive_decode_errors == 1) {
// Only log first error in a sequence
LOG_ERROR("Error receiving frame from decoder: %s", errbuf);
}
}
av_frame_free(&frame);
pthread_mutex_unlock(&ctx->mutex);
moq_consume_frame_close(frame_id);
return;
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure and inspect the relevant region.
git ls-files cpp/obs/src/moq-source.cpp
wc -l cpp/obs/src/moq-source.cpp
sed -n '900,1085p' cpp/obs/src/moq-source.cpp

# Search for decoder receive/send patterns and any drain loops in the repo.
rg -n "avcodec_receive_frame|avcodec_send_packet|consecutive_decode_errors|got_keyframe|avcodec_flush_buffers" cpp/obs/src cpp/obs -g '!**/build/**' -g '!**/dist/**'

Repository: moq-dev/moq

Length of output: 11118


🌐 Web query:

FFmpeg avcodec_send_packet EAGAIN avcodec_receive_frame loop drain multiple frames per packet documentation

💡 Result:

In the FFmpeg send/receive API, avcodec_send_packet and avcodec_receive_frame are decoupled, requiring a specific loop structure to handle multiple frames produced from a single packet or buffering delays [1][2]. When you call avcodec_send_packet, it may return AVERROR(EAGAIN) if the decoder's internal buffers are full and cannot accept more input until you have read existing output [3]. In this state, you must call avcodec_receive_frame in a loop to drain the available frames [1][2]. Once avcodec_receive_frame returns AVERROR(EAGAIN), it indicates that the decoder has no more frames to output and is ready to accept new input, at which point you should resend the packet that previously triggered the EAGAIN error [1][3]. For scenarios where a single input packet contains multiple frames (e.g., certain audio codecs), you must call avcodec_receive_frame repeatedly after a successful avcodec_send_packet until it returns AVERROR(EAGAIN) [1][3]. Only then should you provide the next packet [1][3]. To drain the codec (e.g., at the end of a stream), you must send a NULL packet to avcodec_send_packet to enter draining mode [1][2]. Once in this mode, continue calling avcodec_receive_frame in a loop until it returns AVERROR_EOF [1][2]. Note that while in draining mode, avcodec_receive_frame will not return AVERROR(EAGAIN) [1][2]. Critically, the FFmpeg API is a strict state machine; a codec is never allowed to return AVERROR(EAGAIN) for both sending and receiving simultaneously, as this would create an invalid state and a potential infinite loop [4][2]. Always prioritize draining output frames when the decoder indicates it cannot accept more input [1][3]. Top results: [1][4][2][3][5]

Citations:


Drain and retry the packet instead of dropping it. avcodec_receive_frame() needs a loop until EAGAIN, and a send_packet() EAGAIN means the same packet must be resent after draining. As written, delayed output from codecs with reordering/B-frames can be lost.

🤖 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 `@cpp/obs/src/moq-source.cpp` around lines 961 - 1018, The packet decode path
in moq-source.cpp drops delayed output for codecs with reordering because
avcodec_receive_frame() is only called once and avcodec_send_packet() EAGAIN is
treated as a failure. Update the decode flow around the send/receive logic in
the packet handling function to keep draining avcodec_receive_frame() in a loop
until EAGAIN, and if avcodec_send_packet() returns EAGAIN, drain existing frames
first and then resend the same packet instead of discarding it. Keep the error
handling and consecutive_decode_errors tracking in that same decode path aligned
with the new retry behavior.

Source: Learnings

Comment thread cpp/obs/src/moq-source.cpp Outdated
Comment thread cpp/obs/src/obs-moq.cpp
Comment on lines +1 to +18
/*
SPDX-License-Identifier: GPL-2.0-or-later
Plugin Name
Copyright (C) <Year> <Developer> <Email Address>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the template license boilerplate.

Shipping literal placeholders like <Year> <Developer> <Email Address> leaves the notice inaccurate and diverges from the SPDX-only headers used elsewhere in this vendored tree. Keep the SPDX identifier alone, or replace the template text with the real copyright notice.

🤖 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 `@cpp/obs/src/obs-moq.cpp` around lines 1 - 18, Remove the template copyright
boilerplate from the header in obs-moq.cpp; the literal placeholders in the file
comment are inaccurate. Update the top-of-file comment to keep only the SPDX
identifier or replace the placeholder block with the real copyright notice,
matching the style used elsewhere in the vendored OBS code.

Under GitHub Actions' default `bash -eo pipefail`, `git tag ... | grep -vx
"$cur" | head` fails the step when the inaugural obs-moq-v* release has no
previous tag (grep exits 1). Use awk so an empty result is exit 0.

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

Copy link
Copy Markdown
Collaborator Author

Triage of the CodeRabbit findings.

Fixed here (monorepo-authored):

  • libmoq.yml first-obs-moq-release tag lookup failing under set -eo pipefail — fixed in 85c8e31.

Not fixed here — intentionally routed upstream. Everything under cpp/obs/src/** and cpp/obs/cmake/** is vendored verbatim from moq-dev/obs (this PR only adds SPDX headers + house-style formatting). Patching the logic in-tree would fork the vendored copy and break the re-sync that keeps us current with upstream (which is an explicit goal). CodeRabbit itself notes "coordinate the same fix upstream" on several. So these are tracked for a moq-dev/obs PR, after which we re-vendor:

Real bugs worth an upstream fix:

  • moq-source.cpp frame timestamp µs→ns (1000× off); decode drain loop for B-frames; redact relay URL in logs; ctx free-after-timeout + MoQOutput teardown UAF; close session on publish failure; route terminal callbacks through stop.
  • build.sh macOS triple normalization (only the no---target local path; CI passes --target, so releases are unaffected).
  • CMakePresets.json macOS preset is arm64-only but labeled "Universal" (our CI only ships aarch64, so the artifact is correct; the label is misleading).

OBS-plugintemplate boilerplate (unchanged from upstream obs-plugintemplate, low-risk / likely intended): buildspec_common.cmake execute_process arg handling + unreachable file(REMOVE); FindFFmpeg.cmake pkg-config hinting on macOS; compilerconfig.cmake C-vs-CXX version var; bootstrap.cmake in-source-guard cleanup order; windows/helpers.cmake target param; Qt6 REQUIRED (Qt is OFF).

Leaving these threads open as a tracking signal rather than resolving them.

(Written by Claude Opus 4.8)

kixelated added a commit to moq-dev/obs that referenced this pull request Jul 1, 2026
* Fix source/output lifecycle and correctness bugs

Four safe, self-contained fixes (surfaced by a CodeRabbit review of the
in-tree vendoring in moq-dev/moq#1977):

- moq-source: OBS frame.timestamp is nanoseconds but libmoq delivers
  microseconds, so playback timestamps were 1000x too small, skewing
  async A/V pacing. Multiply by 1000.
- moq-source: redact the relay URL before logging it. URLs can carry
  credentials/tokens and OBS logs are often shared for support.
- moq-source: on the teardown timeout path, leak ctx instead of freeing
  it. If refs is still nonzero a libmoq callback still references ctx
  (mutex/cond/refs), so destroying and freeing it is a use-after-free.
  Leaking on this abnormal path is strictly safer.
- moq-output: close the session when moq_origin_publish fails. The
  session already connected, so returning without closing leaves a live
  background session and a retry on the same output reuses the stale
  handle.

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

* moq-source: strip userinfo at the last '@' when redacting URLs

find('@') would leave part of a password behind for a URL like
moq://user:p@ss@host; rfind('@') removes everything up to the final
authority delimiter.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Pull moq-dev/obs#46 (323d597) into the vendored copy: frame timestamp
µs->ns, relay-URL redaction in logs, leak-not-free on the source teardown
timeout, and close-session-on-publish-failure. Keeps cpp/obs byte-identical
to upstream main (plus the SPDX header).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kixelated kixelated enabled auto-merge (squash) July 1, 2026 03:19
@kixelated kixelated merged commit 484a9e4 into main Jul 1, 2026
2 checks passed
@kixelated kixelated deleted the claude/festive-jepsen-2f7c1c branch July 1, 2026 03:21
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