Skip to content

feat: hi-fi audio - #2305

Open
greenfrvr wants to merge 24 commits into
mainfrom
hifi-audio
Open

feat: hi-fi audio#2305
greenfrvr wants to merge 24 commits into
mainfrom
hifi-audio

Conversation

@greenfrvr

@greenfrvr greenfrvr commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

💡 Overview

Hi-fi audio

  • Disable AEC/NS/AGC for MUSIC_HIGH_QUALITY audio profile, which is defined by settings hifi_audio_enabled flag.
  • Wired stereo input enable/disable flag for Android. For now it is omitted.

Presented media engine

  • Media engine is a layer responsible for creating/disposing WebRTC peer connection factory in runtime during call join/leave stages. Peer connection factory and audio device module instances are defined on per-call basis. Only one factory instance can be created at a time. Web resolves to a no-op engine.
  • Guarded join/leave race. There are several implicit factory creators during join flow: getGenericSdp and initPublisherAndSubscriber (RTCPeerConnection constructor). The guard prevents creating new instance if the join flow was interrupted by leave invocation.

Made RN lobby independent from webrtc

  • Presented new video preview component, which uses video capturer directly without creating local tracks. That video capturer instance is passed later to a local track as a source of media
  • Made device managers mute/unmute on pre-join stage update corresponding state in an optimistic manner. That state is applied during tracks publishing.

Hardened call manager and audio wiring pipeline

  • Public call manager now won't invoke audio session configuration explicitly. Instead it stores configuration params, which are applied during join flow. Those params may override default call settings (e.g. deviceEndpointType). Params are disposed during leave stage.
  • Audio engine interruptions subscription for callingx is now managed during join/leave, instead of single global subscription.

🎫 Ticket: https://linear.app/stream/issue/RN-402/hi-fi-audio

📑 Docs: https://github.com/GetStream/docs-content/pull/1444

Corresponding WebRTC PR: GetStream/react-native-webrtc#50

Summary by CodeRabbit

  • New Features

    • Added per-call WebRTC media engine support with configurable providers.
    • Added React Native lobby camera previews with on-demand permissions and optimistic camera state.
    • Added iOS controls for microphone mute mode and recording preparation.
    • Added audio-engine subscription management for CallingX and in-call behavior.
  • Bug Fixes

    • Prevented join/leave race conditions from continuing setup after leaving.
    • Improved React Native media capture, camera controls, and device-state handling.
    • Improved iOS audio stability when no active audio device is available.

@greenfrvr greenfrvr self-assigned this Jun 25, 2026
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The client adds per-call media-engine creation and disposal, join/leave supersession guards, React Native optimistic device handling, and updated audio-manager orchestration. CallingX gains explicit audio subscription lifecycle methods. The lobby adopts a permission-aware camera preview component.

Changes

Media engine and audio lifecycle

Layer / File(s) Summary
Media engine contract and registration
packages/client/src/rtc/mediaEngine.ts, packages/client/index.ts, packages/client/src/rtc/index.ts, packages/react-native-sdk/src/utils/internal/registerMediaEngine.ts, packages/react-native-sdk/src/utils/internal/registerSDKGlobals.ts
Adds the public media-engine provider contract, registers a React Native CallFactory provider, and exposes the module through client exports.
Call join, leave, and engine ownership
packages/client/src/Call.ts, packages/client/src/devices/__tests__/mocks.ts
Adds cached per-call engine creation and disposal, leave-generation checkpoints, conditional call-manager lifecycle handling, and updated test mocks.
React Native device state and capture
packages/client/src/devices/*Manager.ts
Adds optimistic microphone and camera state reconciliation, React Native pre-join overrides, media-factory-backed capture, speaker setup updates, and bitrate-profile timing validation.
CallingX audio subscription bridge
packages/react-native-callingx/src/*, packages/react-native-callingx/ios/*, packages/react-native-callingx/android/*
Adds explicit wire and unwire APIs across TypeScript, TurboModule, Objective-C, Swift, and Android implementations.
Audio manager and call-manager orchestration
packages/react-native-sdk/src/modules/call-manager/*, packages/react-native-sdk/src/utils/internal/callingx/*, packages/react-native-sdk/src/utils/internal/registerSDKGlobals.ts, packages/react-native-sdk/ios/*
Stores call-manager audio configuration, moves start and stop orchestration into SDK globals, adds mute and recording-prepared bridge methods, and makes ADM access optional.

Lobby camera preview

Layer / File(s) Summary
Permission-aware lobby preview
packages/react-native-sdk/src/components/Call/Lobby/*
Replaces direct RTCView rendering with LobbyCameraPreview, using optimistic camera state, permission checks, call direction, and normalized target resolution.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Call
  participant CallMediaEngineProvider
  participant CallManager
  participant CallingxModule
  Call->>CallMediaEngineProvider: ensureMediaFactory()
  CallMediaEngineProvider-->>Call: create CallFactory-backed engine
  Call->>CallingxModule: wireAudioEngineSubscription()
  Call->>CallManager: start with cid and audio configuration
  Call->>CallingxModule: unwireAudioEngineSubscription()
  Call->>CallMediaEngineProvider: dispose engine
  Call->>CallManager: stop conditionally
Loading

Possibly related PRs

Suggested reviewers: oliverlaz, santhoshvai

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.67% 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 accurately identifies hi-fi audio support, which is a major change in the pull request.
Description check ✅ Passed The description covers the overview, implementation details, ticket, documentation link, and related WebRTC pull request.
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hifi-audio

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.

@greenfrvr
greenfrvr marked this pull request as ready for review July 17, 2026 13:05

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

Actionable comments posted: 4

🧹 Nitpick comments (4)
packages/react-native-sdk/src/utils/internal/registerMediaEngine.ts (1)

37-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate stereo-output-requested derivation across two files. Both sites independently re-derive "is stereo output requested" from the same callManager.getStoredConfig() shape (audioRole === 'listener' && enableStereoAudioOutput === true); a future change to this rule could be applied in one place and missed in the other, causing the factory's voice-processing bypass decision and the native stereo-output enable call to disagree.

  • packages/react-native-sdk/src/utils/internal/registerMediaEngine.ts#L37-L44: extract this predicate into a shared helper (e.g. isStereoOutputRequested(config) exported from the call-manager module) and call it here.
  • packages/react-native-sdk/src/utils/internal/registerSDKGlobals.ts#L105-L108: use the same shared helper instead of re-deriving stereoOutput inline.
🤖 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 `@packages/react-native-sdk/src/utils/internal/registerMediaEngine.ts` around
lines 37 - 44, The stereo-output predicate is duplicated across the media engine
and SDK globals flows. Add and export a shared isStereoOutputRequested(config)
helper from the call-manager module, then use it in
packages/react-native-sdk/src/utils/internal/registerMediaEngine.ts:37-44 and
packages/react-native-sdk/src/utils/internal/registerSDKGlobals.ts:105-108
instead of deriving audioRole and enableStereoAudioOutput inline.
packages/react-native-callingx/ios/CallingxImpl.swift (1)

845-851: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Unresolved TODO on ADM reset removal during audio-session deactivation.

The prior getAudioDeviceModule()?.reset() call was removed and replaced with a commented-out line and an open TODO ("verify if we need to reset the ADM here"). This sits in the post-interruption/call-end audio recovery path. Given the file's own note that "audio recovery is WebRTC's ... we do not touch the session here" this removal is plausible, but the TODO itself signals unresolved uncertainty that should be confirmed (or the dead commented line removed) before merge.

🤖 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 `@packages/react-native-callingx/ios/CallingxImpl.swift` around lines 845 -
851, Resolve the ADM reset uncertainty in provider(_:didDeactivate:) by
confirming whether subscribedADM?.reset() is required after
RTCAudioSession.sharedInstance().audioSessionDidDeactivate(audioSession). If it
is not required, remove the commented-out reset and TODO; otherwise, restore the
reset using the existing ADM symbol and preserve the intended WebRTC
audio-recovery flow.
packages/react-native-callingx/src/CallingxModule.ts (1)

152-163: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Match the class's arrow-function-field convention for consistency.

Every other method on CallingxModule is an arrow-function class field; these two use plain method syntax. No functional bug today since neither references this, but for consistency with the rest of the class:

♻️ Proposed fix
-  wireAudioEngineSubscription(): void {
-    if (Platform.OS !== 'ios') return;
-
-    NativeCallingModule.wireAudioEngineSubscription();
-  }
-
-  unwireAudioEngineSubscription(): void {
-    if (Platform.OS !== 'ios') return;
-
-    NativeCallingModule.unwireAudioEngineSubscription();
-  }
+  wireAudioEngineSubscription = (): void => {
+    if (Platform.OS !== 'ios') return;
+
+    NativeCallingModule.wireAudioEngineSubscription();
+  };
+
+  unwireAudioEngineSubscription = (): void => {
+    if (Platform.OS !== 'ios') return;
+
+    NativeCallingModule.unwireAudioEngineSubscription();
+  };
🤖 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 `@packages/react-native-callingx/src/CallingxModule.ts` around lines 152 - 163,
Convert CallingxModule.wireAudioEngineSubscription and
CallingxModule.unwireAudioEngineSubscription from prototype methods to
arrow-function class fields, preserving their iOS guards and native module
calls.
packages/client/src/devices/MicrophoneManager.ts (1)

193-239: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

New enable/disable/toggle overrides use method syntax instead of arrow-function class fields.

This mirrors an existing pattern in CameraManager, but per coding guidelines all class methods in packages/client/src/ (including overrides) should be arrow-function class fields to preserve automatic this binding. Overload signatures make this awkward with arrow properties (would require a type-literal call-signature declaration), which may be why method syntax was chosen — but it's worth aligning intentionally (here and in CameraManager) rather than by precedent.

As per coding guidelines, "All class methods (including private/protected) must be arrow-function class fields, not method syntax, to preserve this binding automatically across callbacks and observable subscriptions."

🤖 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 `@packages/client/src/devices/MicrophoneManager.ts` around lines 193 - 239,
Convert the MicrophoneManager enable, disable, and toggle overrides from method
syntax to arrow-function class fields so they preserve automatic this binding.
Retain the existing React Native pending-status behavior and super-call
forwarding; represent disable’s overload contract with an appropriate callable
field type, and apply the same alignment to the corresponding CameraManager
overrides.

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 `@packages/client/src/Call.ts`:
- Around line 1225-1244: The join flow in Call.join must treat superseded joins
as failures: add a distinct superseded error and throw it from both existing
supersession bailouts instead of returning normally. Move or guard
callingX.wireAudioEngineSubscription() with the first supersession check so it
cannot reattach the audio engine after leave() has unwired or disposed it, while
preserving normal joining behavior.

In `@packages/client/src/devices/CameraManager.ts`:
- Around line 128-174: Convert enable, disable, toggle, and getStream in
packages/client/src/devices/CameraManager.ts at lines 128-174 and 253-258 to
arrow-function class fields; consolidate disable’s overloads into one arrow
function with a boolean-or-options union parameter while preserving existing
behavior.

In `@packages/client/src/devices/DeviceManager.ts`:
- Around line 512-528: Update reconcileOptimisticStatus so the target ===
'disabled' branch invokes muteStream, matching the behavior of disable(), before
setting the state to disabled. Preserve the existing abort guard and
pending-status reconciliation while ensuring the underlying media tracks are
actually muted or stopped.

In `@packages/react-native-sdk/src/utils/internal/callingx/callingx.ts`:
- Around line 215-237: Wrap the native calls in wireAudioEngineSubscription and
unwireAudioEngineSubscription with try/catch blocks, matching the error-handling
pattern used by registerOutgoingCall, joinCallingxCall, and endCallingxCall.
Preserve the existing setup checks, iOS guards, logging, and native method calls
while ensuring bridge exceptions are caught and handled consistently.

---

Nitpick comments:
In `@packages/client/src/devices/MicrophoneManager.ts`:
- Around line 193-239: Convert the MicrophoneManager enable, disable, and toggle
overrides from method syntax to arrow-function class fields so they preserve
automatic this binding. Retain the existing React Native pending-status behavior
and super-call forwarding; represent disable’s overload contract with an
appropriate callable field type, and apply the same alignment to the
corresponding CameraManager overrides.

In `@packages/react-native-callingx/ios/CallingxImpl.swift`:
- Around line 845-851: Resolve the ADM reset uncertainty in
provider(_:didDeactivate:) by confirming whether subscribedADM?.reset() is
required after
RTCAudioSession.sharedInstance().audioSessionDidDeactivate(audioSession). If it
is not required, remove the commented-out reset and TODO; otherwise, restore the
reset using the existing ADM symbol and preserve the intended WebRTC
audio-recovery flow.

In `@packages/react-native-callingx/src/CallingxModule.ts`:
- Around line 152-163: Convert CallingxModule.wireAudioEngineSubscription and
CallingxModule.unwireAudioEngineSubscription from prototype methods to
arrow-function class fields, preserving their iOS guards and native module
calls.

In `@packages/react-native-sdk/src/utils/internal/registerMediaEngine.ts`:
- Around line 37-44: The stereo-output predicate is duplicated across the media
engine and SDK globals flows. Add and export a shared
isStereoOutputRequested(config) helper from the call-manager module, then use it
in packages/react-native-sdk/src/utils/internal/registerMediaEngine.ts:37-44 and
packages/react-native-sdk/src/utils/internal/registerSDKGlobals.ts:105-108
instead of deriving audioRole and enableStereoAudioOutput inline.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3b9bbdfc-8b86-4e0f-8641-142f75362079

📥 Commits

Reviewing files that changed from the base of the PR and between 4c114ab and 0c456d0.

📒 Files selected for processing (28)
  • packages/client/index.ts
  • packages/client/src/Call.ts
  • packages/client/src/devices/AudioDeviceManager.ts
  • packages/client/src/devices/CameraManager.ts
  • packages/client/src/devices/DeviceManager.ts
  • packages/client/src/devices/MicrophoneManager.ts
  • packages/client/src/devices/ScreenShareManager.ts
  • packages/client/src/devices/SpeakerManager.ts
  • packages/client/src/devices/__tests__/mocks.ts
  • packages/client/src/rtc/index.ts
  • packages/client/src/rtc/mediaEngine.ts
  • packages/client/src/types.ts
  • packages/react-native-callingx/android/src/newarch/java/io/getstream/rn/callingx/CallingxModule.kt
  • packages/react-native-callingx/ios/Callingx.mm
  • packages/react-native-callingx/ios/CallingxImpl.swift
  • packages/react-native-callingx/src/CallingxModule.ts
  • packages/react-native-callingx/src/spec/NativeCallingx.ts
  • packages/react-native-callingx/src/types.ts
  • packages/react-native-sdk/ios/StreamInCallManager.m
  • packages/react-native-sdk/ios/StreamInCallManager.swift
  • packages/react-native-sdk/src/components/Call/Lobby/Lobby.tsx
  • packages/react-native-sdk/src/components/Call/Lobby/LobbyCameraPreview.tsx
  • packages/react-native-sdk/src/components/Call/Lobby/index.ts
  • packages/react-native-sdk/src/modules/call-manager/CallManager.ts
  • packages/react-native-sdk/src/modules/call-manager/native-module.d.ts
  • packages/react-native-sdk/src/utils/internal/callingx/callingx.ts
  • packages/react-native-sdk/src/utils/internal/registerMediaEngine.ts
  • packages/react-native-sdk/src/utils/internal/registerSDKGlobals.ts

Comment on lines +1225 to +1244
const joinLeaveGeneration = this.leaveGeneration;
const supersededByLeave = () =>
this.leaveGeneration !== joinLeaveGeneration;

this.joinCallData = data;

this.logger.debug('Starting join flow');
this.state.setCallingState(CallingState.JOINING);

// Ensure the per-call media engine exists before any peer connection
// (codec probe, subscriber, publisher) or capture happens, so the WebRTC
// globals resolve to the call's factory. Idempotent across
// reconnect/migration attempts.
await this.ensureMediaFactory();

const callingX = globalThis.streamRNVideoSDK?.callingX;
if (callingX) {
callingX.wireAudioEngineSubscription();
}

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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file sections with line numbers.
sed -n '1140,1410p' packages/client/src/Call.ts | cat -n

# Also locate the join/leave generation checks and retry loop entry points.
rg -n "joinLeaveGeneration|supersededByLeave|wireAudioEngineSubscription|unwireAudioEngineSubscription|doJoin|join\(" packages/client/src/Call.ts

Repository: GetStream/stream-video-js

Length of output: 13989


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the join() wrapper and the two bailout regions in doJoin().
sed -n '1100,1405p' packages/client/src/Call.ts | cat -n

# Focus on the exact bailout blocks and any code that runs after them.
sed -n '1298,1395p' packages/client/src/Call.ts | cat -n

# Inspect leave/dispose behavior around the audio-engine unsubscribe path.
sed -n '760,870p' packages/client/src/Call.ts | cat -n

Repository: GetStream/stream-video-js

Length of output: 23162


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for an existing supersession/abort error or related join-flow handling.
rg -n "superseded|abort.*join|join.*abort|JOIN_SUPER|leave superseded|giveUpAndLeave|doJoin\(" packages/client/src

# Inspect any tests or call sites that rely on join() resolving/rejecting in this scenario.
rg -n "call\.join\(|\.join\(\{|\bjoin\(\)" packages/client/src packages/client/test packages/client/src/__tests__ || true

Repository: GetStream/stream-video-js

Length of output: 5013


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the tail of doJoin() and nearby guards that run after the supersession check.
sed -n '1400,1465p' packages/client/src/Call.ts | cat -n

# Inspect the join-rejection caching / retry-related logic referenced in comments.
sed -n '1745,1788p' packages/client/src/Call.ts | cat -n

# Inspect the giveUpAndLeave path inside reconnect handling.
sed -n '1848,2025p' packages/client/src/Call.ts | cat -n

Repository: GetStream/stream-video-js

Length of output: 13668


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where leaveGeneration is incremented/used and how leave synchronizes with join.
rg -n "leaveGeneration|this\.leaveGeneration|generation" packages/client/src/Call.ts

# Inspect the leave implementation around the points that advance generation and dispose the engine.
sed -n '700,860p' packages/client/src/Call.ts | cat -n

# Inspect the join-related call sites that might wait on join/leave sequencing.
sed -n '2035,2145p' packages/client/src/Call.ts | cat -n

Repository: GetStream/stream-video-js

Length of output: 12263


Reject superseded joins instead of resolving them

In packages/client/src/Call.ts, both supersession bailouts currently return normally, so call.join() can resolve even though a concurrent leave() prevented JOINED from ever being set. callingX.wireAudioEngineSubscription() also runs before the first supersession check, which can reattach the audio engine after leave() has already unwired/disposed it. Surface a distinct superseded error from these exits and guard the wiring call.

🤖 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 `@packages/client/src/Call.ts` around lines 1225 - 1244, The join flow in
Call.join must treat superseded joins as failures: add a distinct superseded
error and throw it from both existing supersession bailouts instead of returning
normally. Move or guard callingX.wireAudioEngineSubscription() with the first
supersession check so it cannot reattach the audio engine after leave() has
unwired or disposed it, while preserving normal joining behavior.

Comment thread packages/client/src/devices/CameraManager.ts
Comment on lines +512 to +528
protected reconcileOptimisticStatus = async (): Promise<void> => {
const target = this.state.optimisticStatus;
await withCancellation(this.statusChangeConcurrencyTag, async (signal) => {
try {
if (target === 'enabled' && this.state.status !== 'enabled') {
await this.unmuteStream();
if (!signal.aborted) this.state.setStatus('enabled');
} else if (target === 'disabled' && this.state.status === 'enabled') {
// mirror whatever disable() does to stop/pause the track per disableMode
if (!signal.aborted) this.state.setStatus('disabled');
}
} finally {
if (!signal.aborted) this.state.setPendingStatus(this.state.status);
}
});
};

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.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Missing stream muting when reconciling to disabled state.

When reconciling the status from enabled to disabled, the code only updates the state but fails to actually mute or stop the underlying media tracks. This leaves the camera/microphone physically active despite the SDK reporting them as disabled.

You must invoke muteStream just like the standard disable() method does.

🐛 Proposed fix
   protected reconcileOptimisticStatus = async (): Promise<void> => {
     const target = this.state.optimisticStatus;
     await withCancellation(this.statusChangeConcurrencyTag, async (signal) => {
       try {
         if (target === 'enabled' && this.state.status !== 'enabled') {
           await this.unmuteStream();
           if (!signal.aborted) this.state.setStatus('enabled');
         } else if (target === 'disabled' && this.state.status === 'enabled') {
-          // mirror whatever disable() does to stop/pause the track per disableMode
+          const stopTracks = this.state.disableMode === 'stop-tracks';
+          await this.muteStream(stopTracks);
           if (!signal.aborted) this.state.setStatus('disabled');
         }
       } finally {
         if (!signal.aborted) this.state.setPendingStatus(this.state.status);
       }
     });
   };
📝 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
protected reconcileOptimisticStatus = async (): Promise<void> => {
const target = this.state.optimisticStatus;
await withCancellation(this.statusChangeConcurrencyTag, async (signal) => {
try {
if (target === 'enabled' && this.state.status !== 'enabled') {
await this.unmuteStream();
if (!signal.aborted) this.state.setStatus('enabled');
} else if (target === 'disabled' && this.state.status === 'enabled') {
// mirror whatever disable() does to stop/pause the track per disableMode
if (!signal.aborted) this.state.setStatus('disabled');
}
} finally {
if (!signal.aborted) this.state.setPendingStatus(this.state.status);
}
});
};
protected reconcileOptimisticStatus = async (): Promise<void> => {
const target = this.state.optimisticStatus;
await withCancellation(this.statusChangeConcurrencyTag, async (signal) => {
try {
if (target === 'enabled' && this.state.status !== 'enabled') {
await this.unmuteStream();
if (!signal.aborted) this.state.setStatus('enabled');
} else if (target === 'disabled' && this.state.status === 'enabled') {
const stopTracks = this.state.disableMode === 'stop-tracks';
await this.muteStream(stopTracks);
if (!signal.aborted) this.state.setStatus('disabled');
}
} finally {
if (!signal.aborted) this.state.setPendingStatus(this.state.status);
}
});
};
🤖 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 `@packages/client/src/devices/DeviceManager.ts` around lines 512 - 528, Update
reconcileOptimisticStatus so the target === 'disabled' branch invokes
muteStream, matching the behavior of disable(), before setting the state to
disabled. Preserve the existing abort guard and pending-status reconciliation
while ensuring the underlying media tracks are actually muted or stopped.

Comment thread packages/react-native-sdk/src/utils/internal/callingx/callingx.ts

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

Caution

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

⚠️ Outside diff range comments (3)
packages/react-native-callingx/ios/CallingxImpl.swift (1)

417-422: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset the engine-startup flag when unwiring.

If unwiring occurs after .willEnableAudioEngine and before .willStartAudioEngine, isAudioEngineStarting remains true. The next call can then suppress a genuine system mute action. Reset this flag on pendingActionsQueue in unwireAudioEngineSubscription().

Proposed fix
 engineSubscription?.cancel()
 engineSubscription = nil
 subscribedADM = nil
+pendingActionsQueue.sync {
+    isAudioEngineStarting = false
+}
 CallingxLog.core.debugPublic("[unwireEngineSubscription]")
🤖 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 `@packages/react-native-callingx/ios/CallingxImpl.swift` around lines 417 -
422, Update unwireAudioEngineSubscription() to reset isAudioEngineStarting on
pendingActionsQueue after cancelling the engine subscription, ensuring the
startup state is cleared when unwiring occurs before engine start.
packages/react-native-sdk/ios/StreamInCallManager.swift (2)

426-443: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Persist the forced speaker route.

setForceSpeakerphoneOn does not update state used by applyConfigForEngineEnable(). After an engine rebuild, that method reapplies selectedOutput and can undo the forced speaker route. Store the force state separately, or update the route state only after the native route call succeeds.

🤖 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 `@packages/react-native-sdk/ios/StreamInCallManager.swift` around lines 426 -
443, The setForceSpeakerphoneOn method does not persist the requested
speakerphone state across engine rebuilds. Add or update dedicated force-speaker
state used by applyConfigForEngineEnable(), committing it only after the native
routing operation succeeds, and ensure disabling clears that state so rebuilds
restore the normal selectedOutput route.

219-225: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Check for AudioDeviceModule? before invoking members.

getAudioDeviceModule() returns an optional, but admin, adm.setEngineAvailability(...), and adm?.publisher compile and run even for nil. Guard and unwrap before setup/subscription, then call setEngineAvailability() and bind publisher.

Proposed fix
- let adm = getAudioDeviceModule()
+ guard let adm = getAudioDeviceModule() else { return }
🤖 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 `@packages/react-native-sdk/ios/StreamInCallManager.swift` around lines 219 -
225, Update the no-CallKit audio setup around getAudioDeviceModule() to guard
and unwrap the optional AudioDeviceModule before accessing it. Perform setup,
setEngineAvailability(.default), and publisher binding only within the
successful unwrap path, while preserving the existing behavior when the module
is unavailable.
🤖 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.

Outside diff comments:
In `@packages/react-native-callingx/ios/CallingxImpl.swift`:
- Around line 417-422: Update unwireAudioEngineSubscription() to reset
isAudioEngineStarting on pendingActionsQueue after cancelling the engine
subscription, ensuring the startup state is cleared when unwiring occurs before
engine start.

In `@packages/react-native-sdk/ios/StreamInCallManager.swift`:
- Around line 426-443: The setForceSpeakerphoneOn method does not persist the
requested speakerphone state across engine rebuilds. Add or update dedicated
force-speaker state used by applyConfigForEngineEnable(), committing it only
after the native routing operation succeeds, and ensure disabling clears that
state so rebuilds restore the normal selectedOutput route.
- Around line 219-225: Update the no-CallKit audio setup around
getAudioDeviceModule() to guard and unwrap the optional AudioDeviceModule before
accessing it. Perform setup, setEngineAvailability(.default), and publisher
binding only within the successful unwrap path, while preserving the existing
behavior when the module is unavailable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b0cf985e-a330-405b-a02b-7c6cfb54413e

📥 Commits

Reviewing files that changed from the base of the PR and between f313bfb and b9b4de9.

📒 Files selected for processing (10)
  • packages/client/src/Call.ts
  • packages/react-native-callingx/ios/CallingxImpl.swift
  • packages/react-native-callingx/src/spec/NativeCallingx.ts
  • packages/react-native-callingx/src/types.ts
  • packages/react-native-sdk/ios/StreamInCallManager.m
  • packages/react-native-sdk/ios/StreamInCallManager.swift
  • packages/react-native-sdk/src/modules/call-manager/CallManager.ts
  • packages/react-native-sdk/src/modules/call-manager/native-module.d.ts
  • packages/react-native-sdk/src/utils/internal/callingx/callingx.ts
  • packages/react-native-sdk/src/utils/internal/registerSDKGlobals.ts
💤 Files with no reviewable changes (2)
  • packages/react-native-callingx/src/spec/NativeCallingx.ts
  • packages/react-native-sdk/src/utils/internal/callingx/callingx.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/react-native-sdk/src/modules/call-manager/native-module.d.ts
  • packages/react-native-callingx/src/types.ts
  • packages/react-native-sdk/ios/StreamInCallManager.m
  • packages/react-native-sdk/src/modules/call-manager/CallManager.ts
  • packages/client/src/Call.ts

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

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)
packages/react-native-callingx/ios/CallingxImpl.swift (1)

430-436: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Call unwireAudioEngineSubscription() from leave/reload teardown.

wireAudioEngineSubscription() subscribes to the current ADM, and unwireAudioEngineSubscription() cancels/releases that subscription. There is currently no call path that invokes it when a call/ADM is disposed, so the old subscription can retain the previous ADM and receive later engine events. Wire only when the ADM is ready, and untie the same lifecycle point: leave and JS reload.

🤖 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 `@packages/react-native-callingx/ios/CallingxImpl.swift` around lines 430 -
436, Invoke unwireAudioEngineSubscription() during both call/ADM leave teardown
and JavaScript reload teardown, ensuring it runs before the previous ADM is
disposed or replaced. Keep wireAudioEngineSubscription() gated on ADM readiness
and preserve the existing subscription cleanup behavior.
🤖 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 `@packages/react-native-callingx/ios/CallingxImpl.swift`:
- Around line 991-993: Make updates in setDesiredEngineAvailability atomic with
the reads performed by wireAudioEngineSubscription: serialize the
desiredEngineAvailability assignment, audio-device-module lookup, and
setEngineAvailability replay on the same serial queue or protect both paths with
the same lock, ensuring no stale availability can be applied.

---

Outside diff comments:
In `@packages/react-native-callingx/ios/CallingxImpl.swift`:
- Around line 430-436: Invoke unwireAudioEngineSubscription() during both
call/ADM leave teardown and JavaScript reload teardown, ensuring it runs before
the previous ADM is disposed or replaced. Keep wireAudioEngineSubscription()
gated on ADM readiness and preserve the existing subscription cleanup behavior.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d79e75ab-708d-413e-a59e-15b01b40b534

📥 Commits

Reviewing files that changed from the base of the PR and between b9b4de9 and 7007a8d.

📒 Files selected for processing (5)
  • packages/client/src/types.ts
  • packages/react-native-callingx/ios/CallingxImpl.swift
  • packages/react-native-callingx/src/types.ts
  • packages/react-native-sdk/ios/StreamInCallManager.swift
  • packages/react-native-sdk/src/modules/call-manager/CallManager.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/react-native-callingx/src/types.ts
  • packages/react-native-sdk/ios/StreamInCallManager.swift
  • packages/client/src/types.ts
  • packages/react-native-sdk/src/modules/call-manager/CallManager.ts

Comment on lines +991 to +993
private func setDesiredEngineAvailability(_ availability: RTCAudioEngineAvailability) {
desiredEngineAvailability = availability
_ = getCurrentAudioDeviceModule()?.setEngineAvailability(availability)

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 5 '\b(wireAudioEngineSubscription|setDesiredEngineAvailability|provider\s*\(|methodQueue|dispatch_queue|DispatchQueue\.main)\b' \
  packages/react-native-callingx/ios/CallingxImpl.swift \
  packages/react-native-callingx/ios/Callingx.mm \
  packages/react-native-callingx/src/CallingxModule.ts

Repository: GetStream/stream-video-js

Length of output: 24726


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect queue configuration, property definitions, and the full subscription wiring sequence.
sed -n '360,460p' packages/react-native-callingx/ios/CallingxImpl.swift
sed -n '970,1030p' packages/react-native-callingx/ios/CallingxImpl.swift
rg -n -C 4 'dispatch_queue_t|DispatchQueue|methodQueue|CallingxImpl\(|_moduleImpl|setDesiredEngineAvailability|desiredEngineAvailability|RTCAudioEngineAvailability|getCurrentAudioDeviceModule|engineSubscription|desiredEngineAvailability' \
  packages/react-native-callingx/ios/CallingxImpl.swift \
  packages/react-native-callingx/ios/Callingx.mm \
  packages/react-native-callingx/src/CallingxModule.ts

Repository: GetStream/stream-video-js

Length of output: 45706


Keep desiredEngineAvailability updates atomic.

wireAudioEngineSubscription() reads desiredEngineAvailability, while setDesiredEngineAvailability() writes it after CX provider callbacks. Route that write, the ADM lookup, and the replay through one serial queue, or guard both paths with a lock, to avoid stale availability being applied.

🤖 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 `@packages/react-native-callingx/ios/CallingxImpl.swift` around lines 991 -
993, Make updates in setDesiredEngineAvailability atomic with the reads
performed by wireAudioEngineSubscription: serialize the
desiredEngineAvailability assignment, audio-device-module lookup, and
setEngineAvailability replay on the same serial queue or protect both paths with
the same lock, ensuring no stale availability can be applied.

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.

1 participant