Skip to content

feat(audio): negotiate stereo opus on the subscriber answer - #1007

Open
kirill-jjj wants to merge 4 commits into
livekit:mainfrom
kirill-jjj:stereo-answer-munging
Open

feat(audio): negotiate stereo opus on the subscriber answer#1007
kirill-jjj wants to merge 4 commits into
livekit:mainfrom
kirill-jjj:stereo-answer-munging

Conversation

@kirill-jjj

@kirill-jjj kirill-jjj commented Aug 23, 2026

Copy link
Copy Markdown

Problem

When a remote participant publishes a stereo audio track, Android subscribers receive it as mono. libwebrtc's native Opus decoder downmixes incoming stereo packets to mono unless the local answer negotiates stereo=1 in the fmtp line — even when the server offer correctly announces the track with sprop-stereo=1.

Observed on real hardware (Android 10): the track is published with stereo=1, the server relays sprop-stereo=1 in the subscriber offer — yet the decoded audio plays back mono until stereo=1 is added to the answer.

Related issues

Changes (receive path only)

  • New StereoSdpMunging.kt: ensureStereoOpus() extracts mids carrying sprop-stereo=1 from the server offer and adds stereo=1 to the matching fmtp lines of the answer. Fully fail-safe on parse failures.
  • RTCEngine.onServerOffer munges the answer before setLocalDescription; if setting the munged answer fails, it falls back to the un-munged one (mirrors PeerConnectionTransport.setMungedSdp). The fallback lives in a dedicated helper to keep onServerOffer complexity in check.
  • Changeset (patch — no public API changes).

Scope note (per review)

The publisher-side options (LocalAudioTrackOptions.stereo, TF_STEREO, AddTrackRequest.stereo) were removed from this PR and will follow separately, after useStereoInput/useStereoOutput are exposed by webrtc-sdk/webrtc — so an SDK-level flag can never mismatch what JavaAudioDeviceModule is actually configured to do.

Testing

  • 3 unit tests in livekit-android-test, using SDP fixtures captured from a real LiveKit session
  • Verified on real hardware (Android 10) in a two-participant room: stereo-in confirmed after this change; reproduced mono before it
  • Consumer app built (debug + release, R8) and device-tested against the AAR produced by CI
  • CI green: spotless, build + unit tests, detekt

Assisted by AI; verified end-to-end on real hardware.

@changeset-bot

changeset-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d787073

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@CLAassistant

CLAassistant commented Aug 23, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

devin-ai-integration[bot]

This comment was marked as resolved.

kirill-jjj added a commit to kirill-jjj/client-sdk-android that referenced this pull request Aug 23, 2026
…ment stereo option

Addresses Devin review feedback on livekit#1007:
- if the munged answer is rejected by libwebrtc, retry with the original
  answer and send that instead of leaving the subscriber without a local
  description (mirrors PeerConnectionTransport.setMungedSdp)
- add KDoc to LocalParticipant.AudioTrackPublishOptions.stereo

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

This really first needs a PR on https://github.com/webrtc-sdk/webrtc to expose the useStereoInput/useStereoOutput, because a lot of it creates potential state mismatches between what the JavaAudioDeviceModule is actually configured to do and what the user thinks these options will do.

Separately, I think this might be better served as two separate PRs, one for the subscriber side and one on the publisher side.

}
// Note: stereo is sourced from options, not introspected from
// JavaAudioDeviceModule; callers that enable stereo input via a module
// customizer should set LocalAudioTrackOptions.stereo to match.

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.

this would need to be surfaced to users, and ideally should be sourced from JavaAudioDeviceModule entirely, to avoid a mismatch in state.

/**
* Publish the track as stereo, sent as `stereo` in the AddTrackRequest.
*/
val stereo: Boolean = false,

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.

This should be in BaseAudioTrackPublishOptions, and the stereo option should be added to the end of the list, so that it won't break existing constructors who use positional arguments.

return@launch
}
}
client.sendAnswer(answer, offerId)

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.

rather than nesting the fallback here, should return from the run block with an indicator that it should try the fallback in a separate same-level block.

override val dtx: Boolean = true,
override val red: Boolean = true,
/**
* Publish the track as stereo, sent as `stereo` in the AddTrackRequest.

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.

AddTrackRequest is an internal class the consumers don't need to know about.

@@ -0,0 +1,7 @@
---
'livekit-android': patch

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.

There are API changes in here, so it should be marked as a minor upgrade.

kirill-jjj and others added 3 commits September 8, 2026 22:53
Adds stereo=1 to the Opus fmtp line of the subscriber answer for media
sections where the server offer advertised sprop-stereo=1, so stereo
tracks published by other participants are not downmixed to mono.
Falls back to the un-munged answer if setLocalDescription fails.

Subscriber-only scope; publisher-side stereo options to follow
separately after useStereoInput/useStereoOutput exposure in
webrtc-sdk/webrtc.

Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
onServerOffer exceeded detekt's cyclomatic complexity threshold (16 > 15)
after adding the fallback path.

Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
Generic Either is not smart-cast from an if-check; when() narrows the
Right branch for .value access.

Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
@kirill-jjj
kirill-jjj force-pushed the stereo-answer-munging branch from 0212c81 to 4e07201 Compare September 8, 2026 17:53
devin-ai-integration[bot]

This comment was marked as resolved.

If the answer already carried stereo=0, appending stereo=1 left two
conflicting values and the decoder could stay mono. Drop any existing
stereo=N and append stereo=1.

Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
@kirill-jjj
kirill-jjj requested a review from davidliu September 8, 2026 19:30
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.

Is it possible to add support for Stereo Channel Count in LocalAudioTrackOptions

3 participants