Skip to content

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

Open
kirill-jjj wants to merge 3 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 3 commits into
livekit:mainfrom
kirill-jjj:stereo-answer-munging

Conversation

@kirill-jjj

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, Huawei): stereo capture works (setUseStereoInput(true), confirmed via AudioRecord), the track is published with stereo=1 in AddTrackRequest, the server relays sprop-stereo=1 in the subscriber offer — yet the decoded audio plays back mono until stereo=1 is manually added to the answer.

Related issues

Changes

Receive path (the main fix):

  • New StereoSdpMunging.kt: extracts mids carrying sprop-stereo=1 from the server offer and adds stereo=1 to the matching fmtp lines of the answer. Invoked in RTCEngine.onServerOffer before setLocalDescription/sendAnswer. Fully fail-safe on parse failures.

Publish path (API parity with client-sdk-js):

  • LocalAudioTrackOptions.stereo — capture-side flag, surfaced as the TF_STEREO track feature
  • AudioTrackPublishOptions.stereo — sent as AddTrackRequest.stereo, mirroring JS's forceStereo option

Testing

  • 3 unit tests in livekit-android-test, using SDP fixtures captured from a real device (the subscriber offer from a live LiveKit session)
  • Verified on real hardware (Huawei, Android 10) in a two-participant room: stereo-in/stereo-out confirmed after this change; reproduced mono before it
  • ./gradlew test, spotlessCheck, detektRelease pass

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

The server advertises a stereo publisher track via sprop-stereo=1 in its
offer, but libwebrtc's Opus decoder only decodes incoming packets as
stereo when the local answer's fmtp line also carries stereo=1. Without
it, stereo tracks published by other participants are received as mono.

- StereoSdpMunging: extract sprop-stereo mids from the server offer and
  add stereo=1 to the matching fmtp lines of the answer (mirrors
  client-sdk-js remoteStereoMids handling), applied in onServerOffer
- LocalAudioTrackOptions.stereo + TF_STEREO track feature so publishers
  can declare a stereo capture
- AudioTrackPublishOptions.stereo sent as AddTrackRequest.stereo,
  mirroring client-sdk-js forceStereo

Includes unit tests built from real device SDP captures and a changeset.
@changeset-bot

changeset-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0212c81

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.

…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.

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