Skip to content

Add audio encoder reconfiguration#1362

Open
Qizot wants to merge 1 commit intomoq-dev:mainfrom
Qizot:audio-encoder-reconfigure
Open

Add audio encoder reconfiguration#1362
Qizot wants to merge 1 commit intomoq-dev:mainfrom
Qizot:audio-encoder-reconfigure

Conversation

@Qizot
Copy link
Copy Markdown
Contributor

@Qizot Qizot commented Apr 29, 2026

For some platforms (looking at you Apple) we can't reliably get the channel count from the media track, this information becomes available only when looking at the data inside of audio worklet callback.

The following changes allow for config creation only after receiving the first batch of samples.

For some platforms (looking at you Apple) we can't reliably get the
channel count from the media track, this information becomes available
only when looking at the data inside of audio worklet callback.

The following changes allow for config creation only after receiving the
first batch of samples.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

Walkthrough

The audio encoder's configuration logic has been refactored to use lazy initialization. Instead of setting configuration reactively via #runConfig based on worklet channel count, configuration is now derived from the first captured audio frame. A message handler reads the actual channel count from incoming frame data and invokes a new #createConfig() helper. Configuration initialization within serve() now depends on #config presence, and frame handlers recompute encoder configuration if channel count changes or remains unavailable, resetting lastKeyframe as needed. Channel data handling now uses the complete frame channels array.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add audio encoder reconfiguration' accurately describes the main change—enabling encoder reconfiguration based on actual audio data instead of media track information.
Description check ✅ Passed The description is directly related to the changeset, explaining the platform-specific issue with channel count detection and how the solution defers configuration until the first audio samples arrive.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

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
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
js/publish/src/audio/encoder.ts (1)

135-145: Extract the Opus defaults into named constants.

32_000 and 20 are now part of the encoder policy, so keeping them inline makes later tuning harder and hides intent.

♻️ Suggested refactor
+const OPUS_BITRATE_PER_CHANNEL = 32_000;
+const OPUS_FRAME_DURATION_MS = 20 as Time.Milli;
+
 `#createConfig`(worklet: AudioWorkletNode, channelCount: number): Catalog.AudioConfig {
 	return {
 		codec: "opus",
 		sampleRate: Catalog.u53(worklet.context.sampleRate),
 		numberOfChannels: Catalog.u53(channelCount),
-		bitrate: Catalog.u53(channelCount * 32_000),
+		bitrate: Catalog.u53(channelCount * OPUS_BITRATE_PER_CHANNEL),
 		container: { kind: "legacy" } as const,
 		// TODO parse the actual frame duration instead of assuming 20ms.
 		// Opus supports 2.5–60ms but 20ms is the real-time default.
-		jitter: Catalog.u53(20),
+		jitter: Catalog.u53(OPUS_FRAME_DURATION_MS),
 	};
 }
As per coding guidelines, "Avoid using magic numbers; use named constants instead."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/publish/src/audio/encoder.ts` around lines 135 - 145, Extract the magic
numbers in `#createConfig` into named constants (e.g.,
DEFAULT_OPUS_BITRATE_PER_CHANNEL = 32000 and DEFAULT_OPUS_FRAME_MS = 20) defined
near the top of the module and replace the inline literals (32_000 and 20) in
the bitrate and jitter calculations with those constants (use
Catalog.u53(DEFAULT_OPUS_BITRATE_PER_CHANNEL * channelCount) for bitrate and
Catalog.u53(DEFAULT_OPUS_FRAME_MS) for jitter) so the encoder policy values are
explicit and easier to tune later.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@js/publish/src/audio/encoder.ts`:
- Around line 122-125: The cleanup currently clears the encoder config via
this.#config.set(undefined) but leaves the derived catalog stale; update the
effect.cleanup block (the same place that nulls worklet.port.onmessage and calls
this.#config.set(undefined)) to also reset the derived catalog produced by
`#runCatalog` — e.g., call the catalog-reset method/property (the internal
`#catalog` setter/clear function) so that `#catalog` no longer exposes the previous
rendition after source teardown or reinit.

---

Nitpick comments:
In `@js/publish/src/audio/encoder.ts`:
- Around line 135-145: Extract the magic numbers in `#createConfig` into named
constants (e.g., DEFAULT_OPUS_BITRATE_PER_CHANNEL = 32000 and
DEFAULT_OPUS_FRAME_MS = 20) defined near the top of the module and replace the
inline literals (32_000 and 20) in the bitrate and jitter calculations with
those constants (use Catalog.u53(DEFAULT_OPUS_BITRATE_PER_CHANNEL *
channelCount) for bitrate and Catalog.u53(DEFAULT_OPUS_FRAME_MS) for jitter) so
the encoder policy values are explicit and easier to tune later.
🪄 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: 303c3d0b-f618-4cc8-8dc4-0ca6da4ac682

📥 Commits

Reviewing files that changed from the base of the PR and between 2083546 and fb88a5c.

📒 Files selected for processing (1)
  • js/publish/src/audio/encoder.ts

Comment on lines +122 to +125
effect.cleanup(() => {
worklet.port.onmessage = null;
this.#config.set(undefined);
});
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 29, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clear the derived catalog when the config is reset.

This cleanup drops #config, but #runCatalog() never mirrors that back to #catalog. After source teardown or reinit, catalog will still expose the last rendition until another config arrives.

🩹 Suggested fix
`#runCatalog`(effect: Effect): void {
	const config = effect.get(this.#config);
-	if (!config) return;
+	if (!config) {
+		effect.set(this.#catalog, undefined);
+		return;
+	}

	const catalog: Catalog.Audio = {
		renditions: { [Encoder.TRACK]: config },
	};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/publish/src/audio/encoder.ts` around lines 122 - 125, The cleanup
currently clears the encoder config via this.#config.set(undefined) but leaves
the derived catalog stale; update the effect.cleanup block (the same place that
nulls worklet.port.onmessage and calls this.#config.set(undefined)) to also
reset the derived catalog produced by `#runCatalog` — e.g., call the catalog-reset
method/property (the internal `#catalog` setter/clear function) so that `#catalog`
no longer exposes the previous rendition after source teardown or reinit.

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.

Can you make sure #runCatalog uses this #config signal?

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.

@kixelated, the fix is straightforward — #runCatalog already calls effect.get(this.#config) so it re-runs whenever #config changes, but the early return on a falsy config leaves #catalog stale. Clearing it before returning keeps them in sync:

`#runCatalog`(effect: Effect): void {
    const config = effect.get(this.#config);
    if (!config) {
        effect.set(this.#catalog, undefined);
        return;
    }

    const catalog: Catalog.Audio = {
        renditions: { [Encoder.TRACK]: config },
    };

    effect.set(this.#catalog, catalog);
}

Now when #config is reset to undefined (e.g., during source cleanup), #catalog will immediately be cleared as well instead of exposing a stale rendition.


🧠 Learnings used
Learnt from: kixelated
Repo: moq-dev/moq PR: 948
File: js/publish/vite.config.ts:14-16
Timestamp: 2026-02-13T18:36:03.315Z
Learning: In JS projects that publish web components (custom elements) implemented with a framework (e.g., Solid.js), ensure the framework is bundled into the library output rather than externalized. This means in build config (e.g., vite.config.ts) avoid marking the framework as external and configure the library build to include the framework so consumers do not need to install unused framework dependencies. Verify via build output: the published bundle contains framework code, and no externalized externals for the framework are present. This differs from libraries exporting framework components for direct consumption, where externalizing the framework can be appropriate.


console.debug("encoding audio", config);
encoder.configure(config);
let config = this.#config.peek();
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.

Peek is racey.

If we really need the config, we should break early if effect.get(this.#config) is undefined. This function will rerun when it's set, then we can properly configure the encoder and stuff.


// The information about channels count can be unreliable on different platforms (Apple's safari).
// Try to get the first audio frame and only then create the configuration.
worklet.port.onmessage = ({ data }: { data: Capture.AudioFrame }) => {
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.

It's a little spooky setting onmessage in two places.

You shuld use

effect.event(worklet.port, "message", (data) => {

It'll unregister the event on cleanup too.

Comment on lines +122 to +125
effect.cleanup(() => {
worklet.port.onmessage = null;
this.#config.set(undefined);
});
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.

Can you make sure #runCatalog uses this #config signal?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants