Skip to content

Conversation

@santhoshvai
Copy link
Member

@santhoshvai santhoshvai commented Dec 5, 2025

Summary by CodeRabbit

  • New Features

    • Added comprehensive audio device management with hardware-based echo cancellation and noise suppression.
    • Introduced audio state monitoring and control capabilities.
    • Added audio level monitoring functionality.
  • Bug Fixes

    • Adjusted audio session configuration for improved compatibility.
  • Refactor

    • Restructured audio handling to use modern audio engine architecture.
    • Enhanced audio device initialization process.

✏️ Tip: You can customize this high-level summary in your review settings.

@santhoshvai santhoshvai marked this pull request as ready for review December 9, 2025 15:55
@santhoshvai santhoshvai changed the title initialise peer factory with audio engine feat: stereo audio output support Dec 9, 2025
@santhoshvai
Copy link
Member Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

Warning

Rate limit exceeded

@santhoshvai has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 48 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 3dbc04a and 630250e.

📒 Files selected for processing (1)
  • ios/RCTWebRTC/Utils/AudioDeviceModule/AudioDeviceModule.swift
📝 Walkthrough

Walkthrough

The PR introduces observable audio device state management on iOS via a new Combine-based AudioDeviceModule wrapper, while Android gains a helper method for configuring hardware-accelerated audio processing. Configuration updates include pod dependency bumps, Swift source inclusion, and removal of specific AVAudioSession options.

Changes

Cohort / File(s) Summary
Android Audio Configuration
android/build.gradle
Removed 2 blank lines; formatting-only change.
Android Helper Method
android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java
Added Build import and new private createAudioDeviceModule() helper method to configure JavaAudioDeviceModule with hardware AEC/NS (SDK ≥ Q) and stereo output.
iOS Audio State Abstraction
ios/RCTWebRTC/Utils/AudioDeviceModule/AudioDeviceModule.swift
New large module wrapping RTCAudioDeviceModule with Combine-based observable state (playback, recording, microphone, voice processing, audio levels), event streaming via CurrentValueSubjects, and RTCAudioDeviceModuleDelegate implementation. Includes lifecycle management, audio level tapping, and error handling.
iOS Audio Level Monitoring
ios/RCTWebRTC/Utils/AudioDeviceModule/AudioEngineLevelNodeAdapter.swift
New adapter protocol and implementation for monitoring AVAudioMixerNode levels via input tap, computing RMS-to-dB conversion, and publishing via CurrentValueSubject.
iOS Audio Module Protocol
ios/RCTWebRTC/Utils/AudioDeviceModule/RTCAudioDeviceModuleControlling.swift
New protocol abstracting RTCAudioDeviceModule with observable properties and control methods; extension conformance added to RTCAudioDeviceModule with convenience initAndStartPlayout().
iOS Audio Session & Module Integration
ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m
Removed AVAudioSessionCategoryOptionAllowAirPlay and AVAudioSessionCategoryOptionAllowBluetoothA2DP from audio session category options.
iOS WebRTC Module Header
ios/RCTWebRTC/WebRTCModule.h
Added forward declaration and property for AudioDeviceModule, added localTracks property, removed streamForReactTag: method signature.
iOS WebRTC Module Implementation
ios/RCTWebRTC/WebRTCModule.m
Added Swift header imports (conditional CocoaPods/direct), changed audio device module initialization from RTCAudioDeviceModuleTypePlatformDefault to RTCAudioDeviceModuleTypeAudioEngine, instantiate and assign AudioDeviceModule from factory.
Build & Dependency Configuration
package.json, stream-react-native-webrtc.podspec
Version bumped to 137.1.0-alpha.2; podspec expanded source files glob to include \*.mm and \*.swift, set swift_version to 5.0, and bumped StreamWebRTC dependency to ~137.0.54.

Sequence Diagram(s)

sequenceDiagram
    participant WRC as WebRTCModule
    participant RTCM as RTCAudioDeviceModule
    participant ADM as AudioDeviceModule
    participant Sub as Subscriber

    WRC->>RTCM: initialize with AudioEngine
    RTCM->>ADM: set as delegate
    
    RTCM->>ADM: didCreateEngine(engine)
    ADM->>ADM: update engine reference
    ADM->>Sub: emit didCreateAudioEngine event
    
    RTCM->>ADM: willEnableEngine(...)
    ADM->>Sub: emit willEnableAudioEngine event
    
    RTCM->>ADM: willStartEngine(...)
    ADM->>ADM: installAudioLevelTap()
    ADM->>Sub: emit willStartAudioEngine event
    
    RTCM->>ADM: didReceiveSpeechActivityEvent(...)
    ADM->>Sub: emit speechActivityEvent
    
    RTCM->>ADM: didUpdateAudioProcessingState(...)
    ADM->>ADM: update voice processing state
    ADM->>Sub: publish isVoiceProcessingEnabled, etc.
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 Hops through audio streams with glee,
Combine observers dance with thee,
States and events flow swift and free,
Hardware AEC, now we see—
WebRTC singing harmony! 🎵

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately reflects the main objective of the changeset: introducing stereo audio output support across both iOS and Android platforms.
Docstring Coverage ✅ Passed Docstring coverage is 88.46% which is sufficient. The required threshold is 80.00%.

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


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.

@santhoshvai
Copy link
Member Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

✅ Actions performed

Full review triggered.

Copy link

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

Caution

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

⚠️ Outside diff range comments (1)
ios/RCTWebRTC/WebRTCModule.m (1)

85-110: Guard AudioDeviceModule init against nil peerConnectionFactory.audioDeviceModule.

When a custom audioDevice is supplied (lines 96–101), some WebRTC builds do not populate peerConnectionFactory.audioDeviceModule. Passing nil to the non-optional RTCAudioDeviceModule parameter at line 110 will crash when the initializer immediately accesses properties like source.isPlaying (line 204 in AudioDeviceModule.swift). Add a nil check before initialization.

Proposed fix
-        _audioDeviceModule = [[AudioDeviceModule alloc] initWithSource:_peerConnectionFactory.audioDeviceModule];
+        RTCAudioDeviceModule *adm = _peerConnectionFactory.audioDeviceModule;
+        if (adm != nil) {
+            _audioDeviceModule = [[AudioDeviceModule alloc] initWithSource:adm];
+        } else {
+            _audioDeviceModule = nil;
+        }
🤖 Fix all issues with AI agents
In `@ios/RCTWebRTC/Utils/AudioDeviceModule/AudioDeviceModule.swift`:
- Around line 242-257: The setStereoPlayoutPreference method currently discards
the Int return values from source.setMuteMode and
source.setRecordingAlwaysPreparedMode; change these calls to use the existing
throwingExecution pattern (or check the Int result and call the ADM error
logging/throw helper used by setPlayout/setMicrophoneMuted) so failures surface
instead of being ignored; update setStereoPlayoutPreference to call
throwingExecution around setMuteMode and setRecordingAlwaysPreparedMode (or
inspect their return and invoke the same error/log path) before setting
source.prefersStereoPlayout to ensure errors are logged or propagated.
- Around line 197-226: The init(_ source: any RTCAudioDeviceModuleControlling,
...) seeds isVoiceProcessingBypassedSubject with
source.isVoiceProcessingBypassed but then sets source.isVoiceProcessingBypassed
= true without updating the subject, causing a stale published state; after
assigning source.isVoiceProcessingBypassed = true in init, immediately update
isVoiceProcessingBypassedSubject (e.g., call
isVoiceProcessingBypassedSubject.send(true) or set its value) so the publisher
reflects the new state before WebRTC's didUpdateAudioProcessingState callback,
and if forcing bypass globally is not intended, wrap the assignment in a guard
(or document the global behavior) to only set it under the required condition
(e.g., stereo playout).

In `@ios/RCTWebRTC/Utils/AudioDeviceModule/AudioEngineLevelNodeAdapter.swift`:
- Around line 65-68: The uninstall method currently hardcodes removeTap(onBus:
0) causing taps on non-zero buses to remain; update
AudioEngineLevelNodeAdapter.uninstall to call mixer.removeTap(onBus: bus)
(matching the bus used by installInputTap) so the tap installed on the given bus
is removed; keep the existing guard on inputTap and engine checks and pass
through the bus parameter when removing the tap.
🧹 Nitpick comments (2)
ios/RCTWebRTC/Utils/AudioDeviceModule/AudioEngineLevelNodeAdapter.swift (1)

88-115: Consider multi‑channel RMS and reuse Constant.silenceDB.

Lines 105–115 only sample channelData[0] and clamp with a magic -160. For multi‑channel input (stereo), this can under‑report levels. Consider averaging across channels and using Constant.silenceDB for consistency.

♻️ Suggested adjustment
-        vDSP_rmsqv(channelData[0], 1, &rms, frameCount)
+        let channelCount = Int(buffer.format.channelCount)
+        guard channelCount > 0 else { return }
+        var rmsSum: Float = 0
+        for channel in 0..<channelCount {
+            var channelRms: Float = 0
+            vDSP_rmsqv(channelData[channel], 1, &channelRms, frameCount)
+            rmsSum += channelRms
+        }
+        rms = rmsSum / Float(channelCount)
 ...
-        let clampedRMS = max(-160.0, min(0.0, Float(rmsDB)))
+        let clampedRMS = max(Constant.silenceDB, min(0.0, Float(rmsDB)))
ios/RCTWebRTC/WebRTCModule.h (1)

26-39: Consider marking audioDeviceModule nullable to match runtime availability.

When a custom audioDevice is used, the wrapper may not be created (see .m). Exposing this as nullable makes the API contract explicit.

♻️ Suggested tweak
-@property(nonatomic, strong) AudioDeviceModule *audioDeviceModule;
+@property(nonatomic, strong, nullable) AudioDeviceModule *audioDeviceModule;

Comment on lines +242 to +257
/// Switches between stereo and mono playout while keeping the recording
/// state consistent across reinitializations.
/// - Parameter isPreferred: `true` when stereo output should be used.
@objc public func setStereoPlayoutPreference(_ isPreferred: Bool) {
/// - Important: `.voiceProcessing` requires VP to be enabled in order to mute and
/// `.restartEngine` rebuilds the whole graph. Each of them has different issues:
/// - `.voiceProcessing`: as it requires VP to be enabled in order to mute/unmute that
/// means that for outputs where VP is disabled (e.g. stereo) we cannot mute/unmute.
/// - `.restartEngine`: rebuilds the whole graph and requires explicit calling of
/// `initAndStartRecording` .
_ = source.setMuteMode(isPreferred ? .inputMixer : .voiceProcessing)
/// - Important: We can probably set this one to false when the user doesn't have
/// sendAudio capability.
_ = source.setRecordingAlwaysPreparedMode(false)
source.prefersStereoPlayout = isPreferred
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the AudioDeviceModule.swift file and examine its structure
find . -name "AudioDeviceModule.swift" -type f

Repository: GetStream/react-native-webrtc

Length of output: 136


🏁 Script executed:

# Look at the specific lines mentioned in the review
git ls-files | grep -i audiodevicemodule

Repository: GetStream/react-native-webrtc

Length of output: 282


🏁 Script executed:

# Search for the file using fd
fd AudioDeviceModule.swift

Repository: GetStream/react-native-webrtc

Length of output: 134


🏁 Script executed:

# Read the AudioDeviceModule.swift file to examine the method in question
wc -l ios/RCTWebRTC/Utils/AudioDeviceModule/AudioDeviceModule.swift

Repository: GetStream/react-native-webrtc

Length of output: 138


🏁 Script executed:

# Read the file around the mentioned lines
sed -n '230,270p' ios/RCTWebRTC/Utils/AudioDeviceModule/AudioDeviceModule.swift

Repository: GetStream/react-native-webrtc

Length of output: 2075


🏁 Script executed:

# Find the source object declaration and type
head -100 ios/RCTWebRTC/Utils/AudioDeviceModule/AudioDeviceModule.swift

Repository: GetStream/react-native-webrtc

Length of output: 4920


🏁 Script executed:

# Read the RTCAudioDeviceModuleControlling protocol
cat -n ios/RCTWebRTC/Utils/AudioDeviceModule/RTCAudioDeviceModuleControlling.swift

Repository: GetStream/react-native-webrtc

Length of output: 2063


🏁 Script executed:

# Search for throwingExecution helper method
grep -n "throwingExecution" ios/RCTWebRTC/Utils/AudioDeviceModule/AudioDeviceModule.swift

Repository: GetStream/react-native-webrtc

Length of output: 627


🏁 Script executed:

# Search for all usages of setMuteMode in the codebase
rg "setMuteMode" -A 2 -B 2

Repository: GetStream/react-native-webrtc

Length of output: 1305


🏁 Script executed:

# Read the throwingExecution helper implementation
sed -n '557,572p' ios/RCTWebRTC/Utils/AudioDeviceModule/AudioDeviceModule.swift

Repository: GetStream/react-native-webrtc

Length of output: 500


🏁 Script executed:

# Check if there are any comments around setStereoPlayoutPreference about error handling
sed -n '240,260p' ios/RCTWebRTC/Utils/AudioDeviceModule/AudioDeviceModule.swift

Repository: GetStream/react-native-webrtc

Length of output: 1319


🏁 Script executed:

# Search for setRecordingAlwaysPreparedMode usages to see if there's a pattern
rg "setRecordingAlwaysPreparedMode" -A 2 -B 2

Repository: GetStream/react-native-webrtc

Length of output: 1182


🏁 Script executed:

# Check reset() method context
grep -n "func reset" ios/RCTWebRTC/Utils/AudioDeviceModule/AudioDeviceModule.swift -A 3

Repository: GetStream/react-native-webrtc

Length of output: 154


Add error handling to stereo preference setter.

setMuteMode and setRecordingAlwaysPreparedMode return status codes (Int), but their return values are discarded. This is inconsistent with how other similar ADM operations are handled elsewhere in the code (e.g., setPlayout, setMicrophoneMuted use the throwingExecution pattern to surface errors). Consider logging failures or propagating errors so stereo preference changes don't fail silently.

🤖 Prompt for AI Agents
In `@ios/RCTWebRTC/Utils/AudioDeviceModule/AudioDeviceModule.swift` around lines
242 - 257, The setStereoPlayoutPreference method currently discards the Int
return values from source.setMuteMode and source.setRecordingAlwaysPreparedMode;
change these calls to use the existing throwingExecution pattern (or check the
Int result and call the ADM error logging/throw helper used by
setPlayout/setMicrophoneMuted) so failures surface instead of being ignored;
update setStereoPlayoutPreference to call throwingExecution around setMuteMode
and setRecordingAlwaysPreparedMode (or inspect their return and invoke the same
error/log path) before setting source.prefersStereoPlayout to ensure errors are
logged or propagated.

Comment on lines +65 to +68
func uninstall(on bus: Int = 0) {
if let mixer = inputTap, mixer.engine != nil {
mixer.removeTap(onBus: 0)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "AudioEngineLevelNodeAdapter.swift" -type f

Repository: GetStream/react-native-webrtc

Length of output: 146


🏁 Script executed:

cat -n ./ios/RCTWebRTC/Utils/AudioDeviceModule/AudioEngineLevelNodeAdapter.swift

Repository: GetStream/react-native-webrtc

Length of output: 5446


Use the provided bus parameter when removing the tap.

Line 67 hardcodes onBus: 0 instead of using the bus parameter, while installInputTap correctly uses it (line 52). This causes taps installed on non-zero buses to persist after uninstall is called, leaking taps and keeping callbacks running.

🐛 Proposed fix
    func uninstall(on bus: Int = 0) {
        if let mixer = inputTap, mixer.engine != nil {
-            mixer.removeTap(onBus: 0)
+            mixer.removeTap(onBus: bus)
        }
🤖 Prompt for AI Agents
In `@ios/RCTWebRTC/Utils/AudioDeviceModule/AudioEngineLevelNodeAdapter.swift`
around lines 65 - 68, The uninstall method currently hardcodes removeTap(onBus:
0) causing taps on non-zero buses to remain; update
AudioEngineLevelNodeAdapter.uninstall to call mixer.removeTap(onBus: bus)
(matching the bus used by installInputTap) so the tap installed on the given bus
is removed; keep the existing guard on inputTap and engine checks and pass
through the bus parameter when removing the tap.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@santhoshvai santhoshvai merged commit 1e82d99 into master Jan 20, 2026
4 checks passed
@santhoshvai santhoshvai deleted the stereo-ios branch January 20, 2026 15:43
santhoshvai added a commit to GetStream/stream-video-js that referenced this pull request Jan 20, 2026
### 💡 Overview

Apart from stereo audio support, we also ensure that from video client
we make sure that the audio session always configured before joining a
call and then audio session configuration is deactivated after leaving
the call

depends on: GetStream/react-native-webrtc#23

### 📝 Implementation notes

🎫 Ticket: https://linear.app/stream/issue/RN-332

📑 Docs: GetStream/docs-content#899


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Stereo audio output option for listener role
  * In-call manager lifecycle hooks: setup, start, stop
  * Runtime-registered global in-call manager stub

* **Improvements**
  * Centralized and unified audio setup across platforms
  * Debounced audio route-change handling and richer audio logging
  * Safer activity-aware teardown and thread-safe audio controls

* **Chores**
  * Removed legacy audio-state utility
  * Sample apps updated to opt into new lifecycle APIs (no auto-start)

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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