Skip to content

Fix output publish timing across libmoq versions; make H.264/HEVC keyframes self-contained#40

Open
YogiSotho wants to merge 7 commits into
moq-dev:mainfrom
YogiSotho:fix/moq-output-connect-publish
Open

Fix output publish timing across libmoq versions; make H.264/HEVC keyframes self-contained#40
YogiSotho wants to merge 7 commits into
moq-dev:mainfrom
YogiSotho:fix/moq-output-connect-publish

Conversation

@YogiSotho

@YogiSotho YogiSotho commented Jun 4, 2026

Copy link
Copy Markdown

Edit:

Summary

Publish the broadcast from the session status callback at connect time, handling both libmoq callback conventions (0.2.x: 0 = connected; 0.3.x: positive epoch = connected, 0 = terminal clean close) by latching on the first non-negative status.
Make H.264/HEVC keyframes self-contained: prepend the encoder's SPS/PPS (Annex B extra data) to keyframes that lack them, and defer video track creation until the encoder has produced its headers. Without parameter sets in-band — and with no decoder description in the catalog — WebCodecs viewers reject every frame with "a key frame is required after configure()".

The Windows FFmpeg CMake change originally in this PR has been dropped; that's covered more completely by #31.
Validation

Built and tested end-to-end on Windows against libmoq 0.3.1: OBS publisher → moq-relay over QUIC → Chrome viewers via @moq/watch over WebTransport, with viewers joining mid-stream and decoding from the next keyframe. The status-callback handling is written to remain correct against this repo's current 0.2.14 pin.

Original:


## Summary

- publish the broadcast from the session status callback once the session reports connected
- replace the hardcoded Windows FFmpeg `.deps` path with CMake-based header/library discovery

## Why

The output path currently publishes the broadcast before the session has reported a successful connection. This patch moves publication into the session-connected callback so the output path acts only after the session is established, which also matches the general pattern already used by the source path.

On Windows, the plugin build also depended on a pinned `.deps/obs-deps-...` directory name for FFmpeg. That works locally, but it is brittle across dependency revisions. Switching to CMake discovery keeps the build tied to the configured dependency search path instead of one extracted folder name.

## Notes

This change is intentionally scoped to the dependency contract currently vendored by this plugin build. I am not trying to make a broader claim about all libmoq versions or callback semantics outside the version this repository is building against.

If there is a preferred repo-specific pattern for FFmpeg discovery on Windows, I’m happy to adjust the CMake side to match it.

## Validation

- built successfully on Windows with:
  `cmake --build build_x64_26100 --config Release --target obs-moq`

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fbcf24d7-bf5f-4d25-93c4-8b31dfc96582

📥 Commits

Reviewing files that changed from the base of the PR and between 5027442 and da7c44f.

📒 Files selected for processing (2)
  • src/moq-output.cpp
  • src/moq-output.h
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/moq-output.h
  • src/moq-output.cpp

Walkthrough

This PR moves MoQ broadcast publication into a new PublishBroadcast() helper latched by broadcast_published and invoked on the first non-negative session status callback (Start() no longer publishes). Session status handling now logs reconnect/close/error epochs and aborts on publish failure. Start/Stop/Data sequencing was tightened so Stop() clears output_active before ending capture and Data() drops packets when output_active is false. VideoInit() now delays H.264/H.265 track creation until encoder extra-data is present, retrying per-encoder up to a bounded attempt count and clearing retry state on Stop().

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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 and specifically summarizes the two main changes: fixing publish timing for libmoq versions and making H.264/HEVC keyframes self-contained with necessary parameter sets.
Description check ✅ Passed The description thoroughly explains the PR's objectives, implementation approach, and validation, directly addressing the publish timing fix and keyframe parameter-set handling without being off-topic.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Infer (1.2.0)
src/moq-output.cpp

src/moq-output.cpp:1:10: fatal error: 'obs.hpp' file not found
1 | #include <obs.hpp>
| ^~~~~~~~~
1 error generated.
src/moq-output.cpp:38:2-9: ERROR translating statement 'ReturnStmt'
Aborting translation of method 'MoQOutput::PublishBroadcast' in file 'src/moq-output.cpp': "Assert_failure src/clang/cAst_utils.ml:249:53"
Uncaught Internal Error: "Assert_failure src/clang/cAst_utils.ml:249:53"
Error backtrace:
Raised at ClangFrontend__CAst_utils.get_decl_from_typ_ptr in file "src/clang/cAst_utils.ml", line 249, characters 53-65
Called from ClangFrontend__CTrans.CTrans_funct.get_destructor_decl_ref in file "src/clang/cTrans.ml", line 658, characters 12-59
Called from ClangFrontend__CTrans.CTrans_funct.destructor_calls.(fun) in file "src/clang/cTrans.ml", line 2048, characters 12-69
Called from Base__List.rev_filter_map.loop in file "src/list.ml", line 944, characters 13-17
Called from Base__List.filter_map in file "src/list.ml" (inlined), line 951, characters 26-47
Ca

... [truncated 2200 characters] ...

tend__CTrans.CTrans_funct.instruction in file "src/clang/cTrans.ml" (inlined), line 4765, characters 38-71
Called from ClangFrontend__CTrans.CTrans_funct.exec_with_node_creation in file "src/clang/cTrans.ml" (inlined), line 104, characters 20-38
Called from ClangFrontend__CTrans.CTrans_funct.get_clang_stmt_trans in file "src/clang/cTrans.ml" (inlined), line 5395, characters 4-69
Called from ClangFrontend__CTrans.CTrans_funct.get_custom_stmt_trans in file "src/clang/cTrans.ml", line 5401, characters 8-55
Called from ClangFrontend__CTrans.CTrans_funct.exec_trans_instrs.exec_trans_instrs_rev in file "src/clang/cTrans.ml" (inlined), line 5365, characters 28-54
Called from ClangFrontend__CTrans.CTrans_funct.exec_trans_instrs in file "src/clang/cTrans.ml" (inlined), line 5389, characters 6-70
Ca


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

❤️ Share

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

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

Actionable comments posted: 2

🤖 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 `@src/moq-output.cpp`:
- Around line 29-33: Update the two logging calls to include the required
"[obs-moq]" prefix: change the LOG_INFO call that logs the publishing path and
the LOG_ERROR call inside the moq_origin_publish error branch so their messages
begin with "[obs-moq]". Locate the LOG_INFO("Publishing broadcast: %s",
path.c_str()) and LOG_ERROR("Failed to publish broadcast to session: %d",
result) statements (near the moq_origin_publish(origin, path.data(),
path.size(), broadcast) call) and prepend the prefix in their format strings so
they read e.g. LOG_INFO("[obs-moq] ...") and LOG_ERROR("[obs-moq] ...").
- Around line 23-39: The PublishBroadcast method currently short-circuits using
the instance-level flag broadcast_published, causing publishes to be skipped
across session restarts; fix by making the published state session-scoped —
either reset broadcast_published when a session stops/starts (e.g., in your
session Start/Stop handlers) or track the last published session (add a
last_published_session id member and compare it to the current origin/session
before skipping). Ensure PublishBroadcast (and moq_origin_publish call) uses the
current origin/session identity (origin, path) and sets the session-scoped flag
after a successful publish instead of relying on a single lifetime-initialized
broadcast_published.
🪄 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: a85dfec5-1420-459d-a966-0c9692b4bb46

📥 Commits

Reviewing files that changed from the base of the PR and between 29a90c6 and e8ee611.

📒 Files selected for processing (3)
  • CMakeLists.txt
  • src/moq-output.cpp
  • src/moq-output.h

Comment thread src/moq-output.cpp
Comment thread src/moq-output.cpp
Comment on lines +29 to +33
LOG_INFO("Publishing broadcast: %s", path.c_str());

auto result = moq_origin_publish(origin, path.data(), path.size(), broadcast);
if (result < 0) {
LOG_ERROR("Failed to publish broadcast to session: %d", result);

@coderabbitai coderabbitai Bot Jun 4, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add the required [obs-moq] prefix to new log messages.

Line 29 and Line 33 use project logging macros but omit the required prefix, which makes filtering/attribution inconsistent.

Suggested fix
-	LOG_INFO("Publishing broadcast: %s", path.c_str());
+	LOG_INFO("[obs-moq] Publishing broadcast: %s", path.c_str());
@@
-		LOG_ERROR("Failed to publish broadcast to session: %d", result);
+		LOG_ERROR("[obs-moq] Failed to publish broadcast to session: %d", result);

As per coding guidelines, **/*.{cpp,h,hpp} must use LOG_DEBUG(), LOG_INFO(), LOG_WARNING(), LOG_ERROR() with [obs-moq] prefix.

📝 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
LOG_INFO("Publishing broadcast: %s", path.c_str());
auto result = moq_origin_publish(origin, path.data(), path.size(), broadcast);
if (result < 0) {
LOG_ERROR("Failed to publish broadcast to session: %d", result);
LOG_INFO("[obs-moq] Publishing broadcast: %s", path.c_str());
auto result = moq_origin_publish(origin, path.data(), path.size(), broadcast);
if (result < 0) {
LOG_ERROR("[obs-moq] Failed to publish broadcast to session: %d", result);
🤖 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 `@src/moq-output.cpp` around lines 29 - 33, Update the two logging calls to
include the required "[obs-moq]" prefix: change the LOG_INFO call that logs the
publishing path and the LOG_ERROR call inside the moq_origin_publish error
branch so their messages begin with "[obs-moq]". Locate the LOG_INFO("Publishing
broadcast: %s", path.c_str()) and LOG_ERROR("Failed to publish broadcast to
session: %d", result) statements (near the moq_origin_publish(origin,
path.data(), path.size(), broadcast) call) and prepend the prefix in their
format strings so they read e.g. LOG_INFO("[obs-moq] ...") and
LOG_ERROR("[obs-moq] ...").

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The LOG_* macros already prepend [obs-moq] (see src/logger.h, MOQ_LOG); adding it in the format strings would double it. Runtime logs confirm: [obs-moq] MoQ session connected (epoch 1, 926 ms).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check logger.h to confirm LOG_* macros prepend [obs-moq]
fd -i 'logger.h' --exec cat {}

Length of output: 4966


🏁 Script executed:

#!/bin/bash
# Resolve my own review comment on this PR
gh pr view 40 --repo moq-dev/obs --comments

# Find the comment ID for the review comment to resolve
gh api repos/moq-dev/obs/pulls/40/comments --jq '.[] | select(.user.login == "coderabbitai[bot]") | {id: .id, body: .body[0:80]}'

Length of output: 41029


🏁 Script executed:

#!/bin/bash
# Resolve the specific review comment about [obs-moq] prefix (ID: 3358146326)
gh api --method PUT repos/moq-dev/obs/pulls/comments/3358146326/replies \
  --field body="Acknowledged — the `[obs-moq]` prefix is already injected by the `MOQ_LOG` macro in `src/logger.h`, so adding it in format strings would double-prefix. This comment is invalid and is being resolved." 2>/dev/null || true

# Try to resolve using GraphQL (mark as resolved)
gh api graphql -f query='
mutation {
  resolveReviewThread(input: {threadId: "PRRT_kwDON-aK7M5tniPs"}) {
    thread {
      isResolved
    }
  }
}'

Length of output: 1394


🏁 Script executed:

#!/bin/bash
# Get the review thread ID for the comment with ID 3358146326
gh api repos/moq-dev/obs/pulls/40/comments --jq '.[] | select(.id == 3358146326) | {id: .id, in_reply_to_id: .in_reply_to_id, pull_request_review_id: .pull_request_review_id, body: .body[0:80]}'

Length of output: 482


🏁 Script executed:

#!/bin/bash
# Get the node_id of the review comment
gh api repos/moq-dev/obs/pulls/40/comments --jq '.[] | select(.id == 3358146326) | {id: .id, node_id: .node_id}'

# Also try to get review threads via GraphQL to find the correct thread ID
gh api graphql -f query='
{
  repository(owner: "moq-dev", name: "obs") {
    pullRequest(number: 40) {
      reviewThreads(first: 20) {
        nodes {
          id
          isResolved
          comments(first: 1) {
            nodes {
              databaseId
              body
            }
          }
        }
      }
    }
  }
}'  | jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.comments.nodes[0].databaseId == 3358146326) | {threadId: .id, isResolved: .isResolved}'

Length of output: 897


🏁 Script executed:

#!/bin/bash
# Resolve the review thread using the correct thread ID
gh api graphql -f query='
mutation {
  resolveReviewThread(input: {threadId: "PRRT_kwDOQZh8Os6HKfYG"}) {
    thread {
      isResolved
    }
  }
}'

Length of output: 724



Unfortunately the GitHub API returns a FORBIDDEN error when trying to resolve review threads programmatically via the integration token — this is a GitHub permission limitation. You'll need to resolve this comment manually by clicking the "Resolve conversation" button on the thread in the PR.

The learning has been saved so this mistake won't recur in future reviews: the LOG_* macros in this repo already inject [obs-moq] via MOQ_LOG, and callers should not include it in format strings.


🧬 Code Graph Analysis Results

src/moq-output.h

Lines 9-58

class MoQOutput
{
      public:
    MoQOutput(obs_data_t *settings, obs_output_t *output);
    ~MoQOutput();

    bool Start();
    void Stop(bool signal = true);
    void Data(struct encoder_packet *packet);

    inline size_t GetTotalBytes()
    {
        return total_bytes_sent;
    }

    inline int GetConnectTime()
    {
        return connect_time_ms;
    }

      private:
    bool PublishBroadcast();
    void VideoInit(obs_encoder_t *encoder);
    void VideoData(struct encoder_packet *packet);
    void AudioInit(obs_encoder_t *encoder);
    void AudioData(struct encoder_packet *packet);

    obs_output_t *output;

    std::string server_url;
    std::string path;

    size_t total_bytes_sent;
    int connect_time_ms;
    std::chrono::steady_clock::time_point connect_start;

    int origin;
    int session;
    int broadcast;
    bool broadcast_published;
    // Set once the session status callback first reports a non-negative code;
    // distinguishes "connected" from "closed" across libmoq callback conventions.
    bool session_connected = false;
    std::map<obs_encoder_t *, int> video_tracks;
    std::map<obs_encoder_t *, int> audio_tracks;
    // Per-encoder count of deferred VideoInit attempts while waiting for the encoder's
    // codec headers (SPS/PPS), so a broken encoder surfaces an error instead of
    // silently dropping video forever.
    std::map<obs_encoder_t *, int> video_init_attempts;
}

src/moq-output.cpp

Lines 23-39

bool MoQOutput::PublishBroadcast()
{
	if (broadcast_published) {
		return true;
	}

	LOG_INFO("Publishing broadcast: %s", path.c_str());

	auto result = moq_origin_publish(origin, path.data(), path.size(), broadcast);
	if (result < 0) {
		LOG_ERROR("Failed to publish broadcast to session: %d", result);
		return false;
	}

	broadcast_published = true;
	return true;
}

Lines 106-135

auto session_connect_callback = [](void *user_data, int error_code) {
		auto self = static_cast<MoQOutput *>(user_data);

		if (error_code >= 0 && !self->session_connected) {
			self->session_connected = true;

			if (!self->PublishBroadcast()) {
				obs_output_signal_stop(self->output, OBS_OUTPUT_ERROR);
				return;
			}

			auto elapsed = std::chrono::steady_clock::now() - self->connect_start;
			self->connect_time_ms = static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count());
			LOG_INFO("MoQ session connected (status %d, %d ms): %s", error_code, self->connect_time_ms,
				 self->server_url.c_str());
		} else if (error_code > 0) {
			// 0.3.x reconnect epoch; the broadcast publish is latched, so just log.
			LOG_INFO("MoQ session reconnected (epoch %d): %s", error_code, self->server_url.c_str());
		} else if (error_code == 0) {
			// Terminal clean close: the response to our own moq_session_close()
			// in Stop(), which already signals OBS — don't signal again.
			LOG_INFO("MoQ session closed cleanly: %s", self->server_url.c_str());
		} else {
			// Terminal failure: libmoq's internal reconnect loop gave up. Tell
			// OBS the connection is gone so its reconnect/stop handling runs
			// instead of streaming into a dead session.
			LOG_ERROR("MoQ session failed (%d): %s", error_code, self->server_url.c_str());
			obs_output_signal_stop(self->output, OBS_OUTPUT_DISCONNECTED);
		}
	};

Lines 49-148 (summary)

/**
 * MoQOutput::Start()
 * - Reads OBS output/service connect info into `server_url` and optional `path`.
 * - Checks video encoder availability and initializes OBS encoders.
 * - Sets `connect_start = steady_clock::now()` and `session_connected = false`.
 * - Defines `session_connect_callback` with libmoq-version-dependent conventions:
 *   - First non-negative callback treated as "connected" (and triggers PublishBroadcast()).
 *   - error_code > 0 logs "reconnected (epoch ...)".
 *   - error_code == 0 logs "closed cleanly".
 *   - error_code < 0 logs "failed" and signals OBS_OUTPUT_DISCONNECTED.
 * - Calls moq_session_connect(...) to start the session and returns false on failure.
 * - Calls obs_output_begin_data_capture(output, 0) after initiating the MoQ session.
 * Returns: bool (true if session connect init succeeds and capture begins; otherwise false).
 * Exceptions: none shown.
 */

@danrossi

danrossi commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

The Cmake config fixes is in this PR for windows and mac. As it seems to be auto-generated and tested for Linux pkgconfig resolving. These resolve from the OBS deps that is already pulled while configuring. There was other changes required for windows like a debug console window needed.

#31

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/moq-output.cpp (1)

294-300: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Count the bytes actually published after header prepending.

Once with_headers is used, moq_publish_media_frame() sends payload_size bytes but the stats still add only packet->size. That underreports output throughput on every rewritten keyframe.

Suggested fix
-	total_bytes_sent += packet->size;
+	total_bytes_sent += payload_size;
🤖 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 `@src/moq-output.cpp` around lines 294 - 300, The code updates total_bytes_sent
using packet->size but when with_headers is enabled moq_publish_media_frame()
actually publishes payload_size bytes (headers prepended), causing underreported
throughput; update the accounting in the block after calling
moq_publish_media_frame(handle, payload, payload_size, pts_us) so that
total_bytes_sent increments by payload_size (or by the actual bytes
returned/accepted by moq_publish_media_frame if it provides a byte-count result)
instead of packet->size, referencing the moq_publish_media_frame call, the
payload_size variable, and total_bytes_sent to locate where to change the
increment.
♻️ Duplicate comments (2)
src/moq-output.cpp (2)

56-58: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope broadcast publication to the connection epoch, not the object lifetime.

The new callback retries publication on every positive session epoch, but PublishBroadcast() still permanently short-circuits after the first success. A reconnect, or a later reuse of the same MoQOutput instance, can therefore skip moq_origin_publish(...) entirely and leave the new connection unpublished. Reset this state when a new epoch/session starts, or track the last published epoch instead of a lifetime bool.

Also applies to: 136-139

🤖 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 `@src/moq-output.cpp` around lines 56 - 58, The code currently uses the
lifetime boolean broadcast_published in PublishBroadcast() to permanently
short-circuit moq_origin_publish calls; change this to be scoped to the
connection/session epoch by replacing the bool with an epoch-tracking field
(e.g., last_published_epoch) on MoQOutput and check/reset against the current
session epoch when PublishBroadcast() runs (or update last_published_epoch when
publication succeeds); ensure the checks at both the earlier location (lines
~56-58) and the other occurrence (lines ~136-139) compare against and update
last_published_epoch instead of using broadcast_published so a reconnect or new
session will attempt moq_origin_publish again.

144-149: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Prefix the new log messages with [obs-moq].

These added log strings still miss the required prefix, which makes output-side filtering inconsistent with the rest of the plugin. As per coding guidelines, **/*.{cpp,h,hpp} must use LOG_DEBUG(), LOG_INFO(), LOG_WARNING(), LOG_ERROR() with [obs-moq] prefix.

Also applies to: 348-352

🤖 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 `@src/moq-output.cpp` around lines 144 - 149, The three LOG_INFO calls that log
session connect/close/failure are missing the required "[obs-moq]" prefix;
update each format string passed to LOG_INFO in the session logging block (the
calls referencing connect_time_ms, server_url and error_code) to start with
"[obs-moq] " (e.g. "[obs-moq] MoQ session connected ..."), and make the same
change for the other similar log statements later in the file (the LOG_* calls
around the other session handling block that reference server_url and
error_code) so all plugin logs use the consistent "[obs-moq]" prefix.

Source: Coding guidelines

🤖 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 `@src/moq-output.cpp`:
- Around line 12-39: The annexb_has_sps function currently returns true as soon
as it sees an SPS (NAL type 7) which can skip header injection even if PPS (NAL
type 8) is missing; change annexb_has_sps to scan until the first slice NAL
while tracking both found_sps and found_pps flags (set found_sps when
nal_type==7 and found_pps when nal_type==8) and only return true when both flags
are set; continue returning false immediately on encountering slice NALs
(nal_type==1 or 5) and otherwise advance by hdr+1; apply the same PPS-aware
logic to the other identical check referenced around lines 279-281 (i.e., ensure
both SPS and PPS are present before skipping injection).

---

Outside diff comments:
In `@src/moq-output.cpp`:
- Around line 294-300: The code updates total_bytes_sent using packet->size but
when with_headers is enabled moq_publish_media_frame() actually publishes
payload_size bytes (headers prepended), causing underreported throughput; update
the accounting in the block after calling moq_publish_media_frame(handle,
payload, payload_size, pts_us) so that total_bytes_sent increments by
payload_size (or by the actual bytes returned/accepted by
moq_publish_media_frame if it provides a byte-count result) instead of
packet->size, referencing the moq_publish_media_frame call, the payload_size
variable, and total_bytes_sent to locate where to change the increment.

---

Duplicate comments:
In `@src/moq-output.cpp`:
- Around line 56-58: The code currently uses the lifetime boolean
broadcast_published in PublishBroadcast() to permanently short-circuit
moq_origin_publish calls; change this to be scoped to the connection/session
epoch by replacing the bool with an epoch-tracking field (e.g.,
last_published_epoch) on MoQOutput and check/reset against the current session
epoch when PublishBroadcast() runs (or update last_published_epoch when
publication succeeds); ensure the checks at both the earlier location (lines
~56-58) and the other occurrence (lines ~136-139) compare against and update
last_published_epoch instead of using broadcast_published so a reconnect or new
session will attempt moq_origin_publish again.
- Around line 144-149: The three LOG_INFO calls that log session
connect/close/failure are missing the required "[obs-moq]" prefix; update each
format string passed to LOG_INFO in the session logging block (the calls
referencing connect_time_ms, server_url and error_code) to start with "[obs-moq]
" (e.g. "[obs-moq] MoQ session connected ..."), and make the same change for the
other similar log statements later in the file (the LOG_* calls around the other
session handling block that reference server_url and error_code) so all plugin
logs use the consistent "[obs-moq]" prefix.
🪄 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: cf47460f-f2bf-488e-aa34-cb7a98384396

📥 Commits

Reviewing files that changed from the base of the PR and between e8ee611 and 53937d9.

📒 Files selected for processing (2)
  • src/moq-output.cpp
  • src/moq-output.h

Comment thread src/moq-output.cpp Outdated
Comment on lines +12 to +39
// Does the head of this Annex B H.264 keyframe already carry an SPS NAL (type 7)?
// Parameter sets always precede the IDR slice when present, so stop scanning at
// the first slice NAL.
static bool annexb_has_sps(const uint8_t *data, size_t size)
{
for (size_t i = 0; i + 4 < size;) {
if (data[i] != 0x00 || data[i + 1] != 0x00) {
i++;
continue;
}
size_t hdr;
if (data[i + 2] == 0x01) {
hdr = 3;
} else if (data[i + 2] == 0x00 && data[i + 3] == 0x01) {
hdr = 4;
} else {
i++;
continue;
}
uint8_t nal_type = data[i + hdr] & 0x1f;
if (nal_type == 7)
return true;
if (nal_type == 1 || nal_type == 5)
return false;
i += hdr + 1;
}
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Require PPS too before skipping H.264 header injection.

annexb_has_sps() returns true as soon as it sees NAL type 7, so the H.264 path skips prepending even when the keyframe still lacks PPS (type 8). That leaves a subset of keyframes undecodable even though the encoder extra data already contains the missing parameter set.

Suggested fix
-static bool annexb_has_sps(const uint8_t *data, size_t size)
+static bool annexb_has_sps_pps(const uint8_t *data, size_t size)
 {
+	bool has_sps = false;
+	bool has_pps = false;
+
 	for (size_t i = 0; i + 4 < size;) {
 		if (data[i] != 0x00 || data[i + 1] != 0x00) {
 			i++;
@@
-		uint8_t nal_type = data[i + hdr] & 0x1f;
-		if (nal_type == 7)
-			return true;
-		if (nal_type == 1 || nal_type == 5)
-			return false;
+		uint8_t nal_type = data[i + hdr] & 0x1f;
+		if (nal_type == 7)
+			has_sps = true;
+		else if (nal_type == 8)
+			has_pps = true;
+		if (nal_type == 1 || nal_type == 5)
+			return has_sps && has_pps;
 		i += hdr + 1;
 	}
-	return false;
+	return has_sps && has_pps;
 }
@@
-		if ((is_h264 && !annexb_has_sps(packet->data, packet->size)) || is_hevc) {
+		if ((is_h264 && !annexb_has_sps_pps(packet->data, packet->size)) || is_hevc) {

Also applies to: 279-281

🤖 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 `@src/moq-output.cpp` around lines 12 - 39, The annexb_has_sps function
currently returns true as soon as it sees an SPS (NAL type 7) which can skip
header injection even if PPS (NAL type 8) is missing; change annexb_has_sps to
scan until the first slice NAL while tracking both found_sps and found_pps flags
(set found_sps when nal_type==7 and found_pps when nal_type==8) and only return
true when both flags are set; continue returning false immediately on
encountering slice NALs (nal_type==1 or 5) and otherwise advance by hdr+1; apply
the same PPS-aware logic to the other identical check referenced around lines
279-281 (i.e., ensure both SPS and PPS are present before skipping injection).

…conventions

libmoq 0.2.x reports 0 = connected, while 0.3.x reports positive connection epochs with 0 reserved for the terminal clean close. The previous commit assumed 0.3.x semantics, which would break broadcast publication against the 0.2.14 pin (publish would never run). Treat the first non-negative status as connected so the broadcast is published at connect time under either convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@YogiSotho YogiSotho closed this Jun 10, 2026
@YogiSotho YogiSotho deleted the fix/moq-output-connect-publish branch June 10, 2026 01:21
@YogiSotho YogiSotho restored the fix/moq-output-connect-publish branch June 10, 2026 01:21
The FFmpeg header/library discovery change overlaps with the earlier, more complete Windows build fix in PR moq-dev#31 (which also includes setup docs). Scope this branch to the output publish-timing and H.264/HEVC keyframe fixes; defer the Windows FFmpeg build question to moq-dev#31.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@YogiSotho YogiSotho changed the title Fix output publish timing and Windows FFmpeg lookup Fix output publish timing across libmoq versions; make H.264/HEVC keyframes self-contained Jun 10, 2026
@YogiSotho

YogiSotho commented Jun 10, 2026

Copy link
Copy Markdown
Author

FYI — I dropped the Windows FFmpeg discovery change from #40 since PR #31 covers it more completely; #40 is now scoped to output publish timing and keyframe parameter sets.

@YogiSotho YogiSotho reopened this Jun 10, 2026
Comment thread src/moq-output.cpp Outdated
// Does the head of this Annex B H.264 keyframe already carry an SPS NAL (type 7)?
// Parameter sets always precede the IDR slice when present, so stop scanning at
// the first slice NAL.
static bool annexb_has_sps(const uint8_t *data, size_t size)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This should be in moq-mux? I'm not exactly sure what's being done here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

You're right — this is redundant. We hit DataError: key frame required / fill out the description field on an avc3 track and I assumed keyframes were leaving without parameter sets. On closer testing, moq-mux already re-inserts the cached SPS/PPS before keyframes (#1030); the actual bug was the track being created while obs_encoder_get_extra_data was still empty — no init means nothing to cache and no catalog rendition. I'll drop annexb_has_sps and the prepend, and scope this PR to publish timing + deferring track creation until the encoder headers exist.

might be lost, moment,

moq-mux caches the init parameter sets and re-inserts them before keyframes (#1030), so the plugin-side annexb_has_sps scan and extra-data prepend are redundant. The publisher-side bug that remains fixed here is creating the track while obs_encoder_get_extra_data is still empty, which leaves the importer with nothing to cache.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/moq-output.cpp (2)

298-298: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add the required [obs-moq] prefix to new log messages.

Lines 298 and 301-302 are new log statements that omit the required [obs-moq] prefix.

📝 Proposed fix
 		if (attempts <= max_attempts) {
-			LOG_WARNING("Video codec headers (SPS/PPS) not ready yet; deferring track init (attempt %d)", attempts);
+			LOG_WARNING("[obs-moq] Video codec headers (SPS/PPS) not ready yet; deferring track init (attempt %d)", attempts);
 			return;
 		}
-		LOG_ERROR("Video codec headers (SPS/PPS) still missing after %d attempts; publishing track without them (video may fail to decode)",
+		LOG_ERROR("[obs-moq] Video codec headers (SPS/PPS) still missing after %d attempts; publishing track without them (video may fail to decode)",
 			  attempts);

As per coding guidelines, **/*.{cpp,h,hpp} must use logging macros with [obs-moq] prefix.

Also applies to: 301-302

🤖 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 `@src/moq-output.cpp` at line 298, The new logging calls are missing the
required "[obs-moq]" prefix; update the three new log statements (the
LOG_WARNING call with message "Video codec headers (SPS/PPS) not ready yet;
deferring track init (attempt %d)" and the two other new log statements
introduced nearby) to include the "[obs-moq] " prefix at the start of their
message strings so they conform to the project's logging macro convention (e.g.,
change the format string to "[obs-moq] Video codec headers (SPS/PPS) not ready
yet; deferring track init (attempt %d)").

Source: Coding guidelines


106-129: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stop the output on session failures and clean closes.

The session callback correctly handles the initial connection (line 109) and reconnects (line 121), but terminal conditions are only logged without stopping the output:

  • Line 124-125: Clean close (code == 0 after connected in libmoq 0.3.x) logs but leaves output running
  • Line 126-128: Failures (code < 0 in both versions) log but don't stop the output

Both are terminal conditions per the PR objectives ("0 = terminal clean close", "negative = terminal error"). The output should call obs_output_signal_stop(self->output, ...) with an appropriate code when these occur, similar to how the publish-failure path at line 113 already stops the output.

🛑 Proposed fix
 		} else if (error_code > 0) {
 			// 0.3.x reconnect epoch; the broadcast publish is latched, so just log.
 			LOG_INFO("MoQ session reconnected (epoch %d): %s", error_code, self->server_url.c_str());
 		} else if (error_code == 0) {
-			LOG_INFO("MoQ session closed cleanly: %s", self->server_url.c_str());
+			LOG_INFO("[obs-moq] MoQ session closed cleanly: %s", self->server_url.c_str());
+			obs_output_signal_stop(self->output, OBS_OUTPUT_DISCONNECTED);
 		} else {
-			LOG_INFO("MoQ session failed (%d): %s", error_code, self->server_url.c_str());
+			LOG_ERROR("[obs-moq] MoQ session failed (%d): %s", error_code, self->server_url.c_str());
+			obs_output_signal_stop(self->output, OBS_OUTPUT_ERROR);
 		}
🤖 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 `@src/moq-output.cpp` around lines 106 - 129, The session_connect_callback
handles connects but does not stop the OBS output on terminal conditions; update
the callback so that when error_code == 0 you call
obs_output_signal_stop(self->output, OBS_OUTPUT_FINISHED) for a clean close, and
when error_code < 0 you call obs_output_signal_stop(self->output,
OBS_OUTPUT_ERROR) for failures (mirroring the PublishBroadcast failure path that
currently calls obs_output_signal_stop(self->output, OBS_OUTPUT_ERROR)); make
these calls in the branches inside session_connect_callback (referencing
session_connect_callback, self->session_connected, self->output, and error_code)
so the output is stopped on terminal close/failure.
♻️ Duplicate comments (2)
src/moq-output.cpp (2)

29-29: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add the required [obs-moq] prefix to new log messages.

Lines 29 and 33 use project logging macros but omit the required [obs-moq] prefix, which makes filtering and attribution inconsistent with the coding guidelines.

📝 Proposed fix
-	LOG_INFO("Publishing broadcast: %s", path.c_str());
+	LOG_INFO("[obs-moq] Publishing broadcast: %s", path.c_str());
 
 	auto result = moq_origin_publish(origin, path.data(), path.size(), broadcast);
 	if (result < 0) {
-		LOG_ERROR("Failed to publish broadcast to session: %d", result);
+		LOG_ERROR("[obs-moq] Failed to publish broadcast to session: %d", result);
 		return false;
 	}

As per coding guidelines, **/*.{cpp,h,hpp} must use logging macros with [obs-moq] prefix.

Also applies to: 33-33

🤖 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 `@src/moq-output.cpp` at line 29, Update the LOG_INFO calls in
src/moq-output.cpp to include the required "[obs-moq]" prefix in their message
strings (e.g., change the LOG_INFO("Publishing broadcast: %s", path.c_str()) and
the other LOG_INFO on line 33 to prepend "[obs-moq] " to the format string) so
that all logging uses the project-standard prefix for filtering and attribution.

Source: Coding guidelines


23-39: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Reset broadcast_published in Start() to allow republish across sessions.

Past review comment marked this issue as "Addressed", but the fix is not present in the code. On a second Start() after Stop(), line 25 short-circuits because broadcast_published remains true from the first session, preventing moq_origin_publish() from being called again even though session_connected is correctly reset at line 97.

🔧 Proposed fix
 	// Path (broadcast name) is optional; an empty string publishes to the unnamed broadcast.
 	const char *path_value = obs_service_get_connect_info(service, OBS_SERVICE_CONNECT_INFO_STREAM_KEY);
 	path = path_value ? path_value : "";
+	broadcast_published = false;
 
 	bool found_encoder = false;
🤖 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 `@src/moq-output.cpp` around lines 23 - 39, The issue is that
broadcast_published remains true across sessions so
MoQOutput::PublishBroadcast() short-circuits and never calls
moq_origin_publish() on subsequent Start()/Stop() cycles; fix by resetting
broadcast_published to false at the start of the Start() method (or immediately
after session_connected is reset) so PublishBroadcast() will attempt to
republish for the new session, referencing the MoQOutput::Start(),
MoQOutput::Stop(), broadcast_published, session_connected and
moq_origin_publish() symbols.
🤖 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 `@src/moq-output.cpp`:
- Line 119: Update the modified LOG_INFO calls to include the required
"[obs-moq]" prefix in their messages; locate the logging statements (e.g., the
LOG_INFO invocation that prints "MoQ session connected (status %d, %d ms): %s"
and the other changed LOG_INFO at the same site) and prepend "[obs-moq] " to the
format string so the logs conform to the coding guideline for .cpp/.h/.hpp
files.

---

Outside diff comments:
In `@src/moq-output.cpp`:
- Line 298: The new logging calls are missing the required "[obs-moq]" prefix;
update the three new log statements (the LOG_WARNING call with message "Video
codec headers (SPS/PPS) not ready yet; deferring track init (attempt %d)" and
the two other new log statements introduced nearby) to include the "[obs-moq] "
prefix at the start of their message strings so they conform to the project's
logging macro convention (e.g., change the format string to "[obs-moq] Video
codec headers (SPS/PPS) not ready yet; deferring track init (attempt %d)").
- Around line 106-129: The session_connect_callback handles connects but does
not stop the OBS output on terminal conditions; update the callback so that when
error_code == 0 you call obs_output_signal_stop(self->output,
OBS_OUTPUT_FINISHED) for a clean close, and when error_code < 0 you call
obs_output_signal_stop(self->output, OBS_OUTPUT_ERROR) for failures (mirroring
the PublishBroadcast failure path that currently calls
obs_output_signal_stop(self->output, OBS_OUTPUT_ERROR)); make these calls in the
branches inside session_connect_callback (referencing session_connect_callback,
self->session_connected, self->output, and error_code) so the output is stopped
on terminal close/failure.

---

Duplicate comments:
In `@src/moq-output.cpp`:
- Line 29: Update the LOG_INFO calls in src/moq-output.cpp to include the
required "[obs-moq]" prefix in their message strings (e.g., change the
LOG_INFO("Publishing broadcast: %s", path.c_str()) and the other LOG_INFO on
line 33 to prepend "[obs-moq] " to the format string) so that all logging uses
the project-standard prefix for filtering and attribution.
- Around line 23-39: The issue is that broadcast_published remains true across
sessions so MoQOutput::PublishBroadcast() short-circuits and never calls
moq_origin_publish() on subsequent Start()/Stop() cycles; fix by resetting
broadcast_published to false at the start of the Start() method (or immediately
after session_connected is reset) so PublishBroadcast() will attempt to
republish for the new session, referencing the MoQOutput::Start(),
MoQOutput::Stop(), broadcast_published, session_connected and
moq_origin_publish() symbols.
🪄 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: bebda621-1f13-476c-a5b5-c087cc66b693

📥 Commits

Reviewing files that changed from the base of the PR and between 53937d9 and 5027442.

📒 Files selected for processing (2)
  • src/moq-output.cpp
  • src/moq-output.h
✅ Files skipped from review due to trivial changes (1)
  • src/moq-output.h

Comment thread src/moq-output.cpp
auto elapsed = std::chrono::steady_clock::now() - self->connect_start;
self->connect_time_ms = static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count());
LOG_INFO("MoQ session established (%d ms): %s", self->connect_time_ms,
LOG_INFO("MoQ session connected (status %d, %d ms): %s", error_code, self->connect_time_ms,

@coderabbitai coderabbitai Bot Jun 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add the required [obs-moq] prefix to changed log messages.

Lines 119 and 123 are new or modified log statements that omit the required [obs-moq] prefix.

📝 Proposed fix
-			LOG_INFO("MoQ session connected (status %d, %d ms): %s", error_code, self->connect_time_ms,
+			LOG_INFO("[obs-moq] MoQ session connected (status %d, %d ms): %s", error_code, self->connect_time_ms,
 				 self->server_url.c_str());
 		} else if (error_code > 0) {
 			// 0.3.x reconnect epoch; the broadcast publish is latched, so just log.
-			LOG_INFO("MoQ session reconnected (epoch %d): %s", error_code, self->server_url.c_str());
+			LOG_INFO("[obs-moq] MoQ session reconnected (epoch %d): %s", error_code, self->server_url.c_str());

As per coding guidelines, **/*.{cpp,h,hpp} must use logging macros with [obs-moq] prefix.

Also applies to: 123-123

🤖 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 `@src/moq-output.cpp` at line 119, Update the modified LOG_INFO calls to
include the required "[obs-moq]" prefix in their messages; locate the logging
statements (e.g., the LOG_INFO invocation that prints "MoQ session connected
(status %d, %d ms): %s" and the other changed LOG_INFO at the same site) and
prepend "[obs-moq] " to the format string so the logs conform to the coding
guideline for .cpp/.h/.hpp files.

Source: Coding guidelines

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The LOG_* macros already prepend [obs-moq] (see src/logger.h, MOQ_LOG); adding it in the format strings would double it. Runtime logs confirm: [obs-moq] MoQ session connected (epoch 1, 926 ms).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i "logger.h" --exec cat {}

Length of output: 3157


🏁 Script executed:

#!/bin/bash
# Resolve the CodeRabbit review comment on PR `#40`
gh api graphql -f query='
mutation {
  resolveReviewThread(input: {threadId: "PRRT_kwDOOeNFbc6OHrGV"}) {
    thread {
      isResolved
    }
  }
}'

Length of output: 754


🏁 Script executed:

#!/bin/bash
gh api graphql -f query='
query {
  repository(owner: "moq-dev", name: "obs") {
    pullRequest(number: 40) {
      reviewThreads(first: 20) {
        nodes {
          id
          isResolved
          comments(first: 1) {
            nodes {
              author { login }
              body
            }
          }
        }
      }
    }
  }
}'

Length of output: 9863


🏁 Script executed:

#!/bin/bash
# Resolve the correct thread - [obs-moq] prefix on lines 119/123
gh api graphql -f query='
mutation {
  resolveReviewThread(input: {threadId: "PRRT_kwDOQZh8Os6IVudf"}) {
    thread {
      isResolved
    }
  }
}'

# Also resolve the same false-positive on lines 29/33
gh api graphql -f query='
mutation {
  resolveReviewThread(input: {threadId: "PRRT_kwDOQZh8Os6HKfYG"}) {
    thread {
      isResolved
    }
  }
}'

Length of output: 1202



@YogiSotho, confirmed — src/logger.h makes it clear:

`#define` MOQ_LOG(level, format, ...) blog(level, "[obs-moq] " format, ##__VA_ARGS__)

The prefix is baked into the macro itself, so callers must not include it in their format strings. My review comments on this (lines 119/123 and lines 29/33) were both false positives — apologies for the noise. I wasn't able to resolve those threads programmatically due to API permissions, but you can dismiss them manually.


🧬 Code Graph Analysis Results

src/moq-output.cpp

[L23-L39] PublishBroadcast() log calls

bool MoQOutput::PublishBroadcast()
{
	if (broadcast_published) {
		return true;
	}

	LOG_INFO("Publishing broadcast: %s", path.c_str());

	auto result = moq_origin_publish(origin, path.data(), path.size(), broadcast);
	if (result < 0) {
		LOG_ERROR("Failed to publish broadcast to session: %d", result);
		return false;
	}

	broadcast_published = true;
	return true;
}

[L49-L98] Start-up connection log

LOG_INFO("Connecting to MoQ server: %s", server_url.c_str());
...
session_connected = false;

[L104-L130] Session status callback: the “MoQ session connected …” log

auto session_connect_callback = [](void *user_data, int error_code) {
		auto self = static_cast<MoQOutput *>(user_data);

		if (error_code >= 0 && !self->session_connected) {
			self->session_connected = true;

			if (!self->PublishBroadcast()) {
				obs_output_signal_stop(self->output, OBS_OUTPUT_ERROR);
				return;
			}

			auto elapsed = std::chrono::steady_clock::now() - self->connect_start;
			self->connect_time_ms = static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count());
			LOG_INFO("MoQ session connected (status %d, %d ms): %s", error_code, self->connect_time_ms,
				 self->server_url.c_str());
		} else if (error_code > 0) {
			// 0.3.x reconnect epoch; the broadcast publish is latched, so just log.
			LOG_INFO("MoQ session reconnected (epoch %d): %s", error_code, self->server_url.c_str());
		} else if (error_code == 0) {
			// Terminal clean close: the response to our own moq_session_close()
			// in Stop(), which already signals OBS — don't signal again.
			LOG_INFO("MoQ session closed cleanly: %s", self->server_url.c_str());
		} else {
			// Terminal failure: libmoq's internal reconnect loop gave up. Tell
			// OBS the connection is gone so its reconnect/stop handling runs
			// instead of streaming into a dead session.
			LOG_ERROR("MoQ session failed (%d): %s", error_code, self->server_url.c_str());
			obs_output_signal_stop(self->output, OBS_OUTPUT_DISCONNECTED);
		}
	};

src/moq-output.h

[L9-L58] Members related to connect-time logging

inline size_t GetTotalBytes()
{
    return total_bytes_sent;
}

inline int GetConnectTime()
{
    return connect_time_ms;
}
...
    int connect_time_ms;
...
    // Set once the session status callback first reports a non-negative code;
    // distinguishes "connected" from "closed" across libmoq callback conventions.
    bool session_connected = false;

✏️ Learnings added
Learnt from: YogiSotho
Repo: moq-dev/obs PR: 40
File: src/moq-output.cpp:119-119
Timestamp: 2026-06-10T02:56:36.239Z
Learning: In the obs-moq OBS plugin (repository: moq-dev/obs), the `LOG_DEBUG()`, `LOG_INFO()`, `LOG_WARNING()`, and `LOG_ERROR()` macros defined in `src/logger.h` automatically prepend `[obs-moq] ` to every message via the underlying `MOQ_LOG` macro: `#define MOQ_LOG(level, format, ...) blog(level, "[obs-moq] " format, ##__VA_ARGS__)`. Callers must NOT include `[obs-moq]` in their format strings — doing so would produce a doubled prefix like `[obs-moq] [obs-moq] ...`. The coding guideline "use logging macros with [obs-moq] prefix" means: use the macros (not raw `blog()`), not that callers must embed the literal prefix string. Any review comment requesting that callers add `[obs-moq]` to their format strings is a false positive.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

YogiSotho and others added 2 commits June 9, 2026 22:30
A negative status is terminal: libmoq's internal reconnect loop has given up. Signal OBS_OUTPUT_DISCONNECTED so OBS's own reconnect/stop handling takes over instead of streaming into a dead session. A 0 status stays log-only: it is the response to our own moq_session_close() in Stop(), which already signals OBS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stop() closed the session and cleared the track maps but never ended data capture, so late encoder packets arriving during teardown re-created tracks ('track initialized' + 'Failed to write video frame' after Streaming Stop in every log). Those zombie tracks survive in the maps and the next session reuses them instead of initializing fresh ones, publishing into tracks created mid-teardown. End data capture first and gate Data() on an output_active flag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@danrossi

Copy link
Copy Markdown
Contributor

The end data capture on stop method may resolve needing to check for an empty map in my PR. Its causing errors to be thrown when republishing in Windows. I didn't know there was such a method.

There is alot added into one PR which Ive tried to break them up personally. But that stop data capture method is needed at least.

#39

@danrossi

Copy link
Copy Markdown
Contributor

This PR already adds the broadcast signals but there is an issue figuring out connection retries internally so it only signals a reconnection once.

#36

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.

3 participants