From 13210327a323c334cfb6316475b3c9d0be6b830f Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Fri, 20 Feb 2026 15:26:29 +0100 Subject: [PATCH 01/12] feat: added audio capture for screen sharing for Android --- .../StreamVideoReactNativeModule.kt | 93 ++++++++++++ .../screenshare/ScreenAudioCapture.kt | 109 ++++++++++++++ .../CallControls/ScreenShareToggleButton.tsx | 12 +- packages/react-native-sdk/src/hooks/index.ts | 1 + .../src/hooks/useScreenShareAudioMixing.ts | 137 ++++++++++++++++++ .../src/hooks/useScreenShareButton.ts | 82 ++++++++++- .../src/native/ScreenShareAudioModule.ts | 47 ++++++ .../src/providers/StreamCall/index.tsx | 11 ++ .../CallControlls/BottomControls.tsx | 4 +- 9 files changed, 492 insertions(+), 4 deletions(-) create mode 100644 packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/screenshare/ScreenAudioCapture.kt create mode 100644 packages/react-native-sdk/src/hooks/useScreenShareAudioMixing.ts create mode 100644 packages/react-native-sdk/src/native/ScreenShareAudioModule.ts diff --git a/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt index 58bf5a48bd..cb1aaff308 100644 --- a/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt +++ b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt @@ -1,5 +1,6 @@ package com.streamvideo.reactnative +import android.app.Activity import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -9,6 +10,7 @@ import android.graphics.Bitmap import android.media.AudioAttributes import android.media.AudioFormat import android.media.AudioTrack +import android.media.projection.MediaProjectionManager import android.net.Uri import android.os.BatteryManager import android.os.Build @@ -23,6 +25,8 @@ import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.WritableMap import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter import com.oney.WebRTCModule.WebRTCModule +import com.oney.WebRTCModule.WebRTCModuleOptions +import com.streamvideo.reactnative.screenshare.ScreenAudioCapture import com.streamvideo.reactnative.util.CallAlivePermissionsHelper import com.streamvideo.reactnative.util.CallAliveServiceChecker import com.streamvideo.reactnative.util.PiPHelper @@ -52,6 +56,9 @@ class StreamVideoReactNativeModule(reactContext: ReactApplicationContext) : private var busyToneAudioTrack: AudioTrack? = null private var busyToneJob: Job? = null + // Screen share audio mixing + private var screenAudioCapture: ScreenAudioCapture? = null + private var thermalStatusListener: PowerManager.OnThermalStatusChangedListener? = null private var batteryChargingStateReceiver = object : BroadcastReceiver() { @@ -148,6 +155,7 @@ class StreamVideoReactNativeModule(reactContext: ReactApplicationContext) : reactApplicationContext.unregisterReceiver(batteryChargingStateReceiver) stopThermalStatusUpdates() stopBusyToneInternal() // Clean up busy tone on invalidate + stopScreenShareAudioMixingInternal() // Clean up screen share audio on invalidate super.invalidate() } @@ -484,6 +492,91 @@ class StreamVideoReactNativeModule(reactContext: ReactApplicationContext) : return ShortArray(totalSamples) } + @ReactMethod + fun startScreenShareAudioMixing(promise: Promise) { + try { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + promise.reject("API_LEVEL", "Screen audio capture requires Android 10 (API 29)+") + return + } + + if (screenAudioCapture != null) { + Log.w(NAME, "Screen share audio mixing is already active") + promise.resolve(null) + return + } + + val webRTCModule = reactApplicationContext.getNativeModule(WebRTCModule::class.java) + if (webRTCModule == null) { + promise.reject("WEBRTC_ERROR", "WebRTCModule not found") + return + } + + // Get the MediaProjection permission result Intent from WebRTC + val permissionIntent = webRTCModule.userMediaImpl?.mediaProjectionPermissionResultData + if (permissionIntent == null) { + promise.reject("NO_PROJECTION", "No MediaProjection permission available. Start screen sharing first.") + return + } + + // Create a MediaProjection for audio capture + val mediaProjectionManager = reactApplicationContext.getSystemService( + Context.MEDIA_PROJECTION_SERVICE + ) as MediaProjectionManager + val mediaProjection = mediaProjectionManager.getMediaProjection( + Activity.RESULT_OK, permissionIntent + ) + if (mediaProjection == null) { + promise.reject("PROJECTION_ERROR", "Failed to create MediaProjection for audio capture") + return + } + + // Create and start screen audio capture + val capture = ScreenAudioCapture(mediaProjection) + capture.start() + screenAudioCapture = capture + + // Register the screen audio bytes provider so the AudioBufferCallback + // in WebRTCModule mixes screen audio into the mic buffer. + WebRTCModuleOptions.getInstance().screenAudioBytesProvider = + WebRTCModuleOptions.ScreenAudioBytesProvider { bytesRequested -> + capture.getScreenAudioBytes(bytesRequested) + } + + Log.d(NAME, "Screen share audio mixing started") + promise.resolve(null) + } catch (e: Exception) { + Log.e(NAME, "Error starting screen share audio mixing: ${e.message}") + promise.reject("ERROR", e.message, e) + } + } + + @ReactMethod + fun stopScreenShareAudioMixing(promise: Promise) { + try { + stopScreenShareAudioMixingInternal() + promise.resolve(null) + } catch (e: Exception) { + Log.e(NAME, "Error stopping screen share audio mixing: ${e.message}") + promise.reject("ERROR", e.message, e) + } + } + + private fun stopScreenShareAudioMixingInternal() { + try { + // Clear the provider so the AudioBufferCallback stops mixing + WebRTCModuleOptions.getInstance().screenAudioBytesProvider = null + + // Stop and release the audio capture + screenAudioCapture?.stop() + screenAudioCapture = null + + Log.d(NAME, "Screen share audio mixing stopped") + } catch (e: Exception) { + Log.e(NAME, "Error in stopScreenShareAudioMixingInternal: ${e.message}") + } + } + companion object { private const val NAME = "StreamVideoReactNative" private const val SAMPLE_RATE = 22050 diff --git a/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/screenshare/ScreenAudioCapture.kt b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/screenshare/ScreenAudioCapture.kt new file mode 100644 index 0000000000..8b15652e36 --- /dev/null +++ b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/screenshare/ScreenAudioCapture.kt @@ -0,0 +1,109 @@ +package com.streamvideo.reactnative.screenshare + +import android.annotation.SuppressLint +import android.media.AudioAttributes +import android.media.AudioFormat +import android.media.AudioPlaybackCaptureConfiguration +import android.media.AudioRecord +import android.media.projection.MediaProjection +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import java.nio.ByteBuffer + +/** + * Captures system media audio using [AudioPlaybackCaptureConfiguration]. + * + * Uses the given [MediaProjection] to set up an [AudioRecord] that captures + * audio from media playback (e.g., YouTube, music apps) but not notifications + * or system sounds. + * + * Audio is captured in a pull-based manner via [getScreenAudioBytes], which + * reads exactly the requested number of bytes using [AudioRecord.READ_BLOCKING]. + * This is designed to be called from the WebRTC audio processing thread. + * + * Format: 48kHz, mono, PCM 16-bit (matching WebRTC's audio pipeline). + * + * Requires Android 10 (API 29+). + */ +@RequiresApi(Build.VERSION_CODES.Q) +class ScreenAudioCapture(private val mediaProjection: MediaProjection) { + + private var audioRecord: AudioRecord? = null + private var screenAudioBuffer: ByteBuffer? = null + + companion object { + private const val TAG = "ScreenAudioCapture" + const val SAMPLE_RATE = 48000 + private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO + private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT + } + + @SuppressLint("MissingPermission") + fun start() { + val playbackConfig = AudioPlaybackCaptureConfiguration.Builder(mediaProjection) + .addMatchingUsage(AudioAttributes.USAGE_MEDIA) + .build() + + val audioFormat = AudioFormat.Builder() + .setSampleRate(SAMPLE_RATE) + .setChannelMask(CHANNEL_CONFIG) + .setEncoding(AUDIO_FORMAT) + .build() + + audioRecord = AudioRecord.Builder() + .setAudioFormat(audioFormat) + .setAudioPlaybackCaptureConfig(playbackConfig) + .build() + + if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { + Log.e(TAG, "AudioRecord failed to initialize") + audioRecord?.release() + audioRecord = null + return + } + + audioRecord?.startRecording() + Log.d(TAG, "Screen audio capture started") + } + + /** + * Pull-based read: returns a [ByteBuffer] containing exactly [bytesRequested] bytes + * of captured screen audio. + * + * Called from the WebRTC audio processing thread. Uses [AudioRecord.READ_BLOCKING] + * so it will block until the requested bytes are available. + * + * @return A [ByteBuffer] with screen audio data, or `null` if capture is not active. + */ + fun getScreenAudioBytes(bytesRequested: Int): ByteBuffer? { + val record = audioRecord ?: return null + if (bytesRequested <= 0) return null + + val buffer = screenAudioBuffer?.takeIf { it.capacity() >= bytesRequested } + ?: ByteBuffer.allocateDirect(bytesRequested).also { screenAudioBuffer = it } + + buffer.clear() + buffer.limit(bytesRequested) + + val bytesRead = record.read(buffer, bytesRequested, AudioRecord.READ_BLOCKING) + if (bytesRead > 0) { + buffer.position(0) + buffer.limit(bytesRead) + return buffer + } + return null + } + + fun stop() { + try { + audioRecord?.stop() + } catch (e: Exception) { + Log.w(TAG, "Error stopping AudioRecord: ${e.message}") + } + audioRecord?.release() + audioRecord = null + screenAudioBuffer = null + Log.d(TAG, "Screen audio capture stopped") + } +} diff --git a/packages/react-native-sdk/src/components/Call/CallControls/ScreenShareToggleButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/ScreenShareToggleButton.tsx index da899f178e..d0899ca9bd 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/ScreenShareToggleButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/ScreenShareToggleButton.tsx @@ -5,7 +5,10 @@ import { ScreenShare } from '../../../icons/ScreenShare'; import { StopScreenShare } from '../../../icons/StopScreenShare'; import { CallControlsButton } from './CallControlsButton'; import { useTheme } from '../../../contexts/ThemeContext'; -import { useScreenShareButton } from '../../../hooks/useScreenShareButton'; +import { + useScreenShareButton, + type ScreenShareOptions, +} from '../../../hooks/useScreenShareButton'; import { IconWrapper } from '../../../icons'; /** @@ -22,6 +25,10 @@ export type ScreenShareToggleButtonProps = { * */ onScreenShareStoppedHandler?: () => void; + /** + * Options for screen share behavior (type, includeAudio). + */ + screenShareOptions?: ScreenShareOptions; }; /** @@ -31,6 +38,7 @@ export type ScreenShareToggleButtonProps = { export const ScreenShareToggleButton = ({ onScreenShareStartedHandler, onScreenShareStoppedHandler, + screenShareOptions, }: ScreenShareToggleButtonProps) => { const { theme: { colors, screenShareToggleButton, variants }, @@ -42,6 +50,8 @@ export const ScreenShareToggleButton = ({ screenCapturePickerViewiOSRef, onScreenShareStartedHandler, onScreenShareStoppedHandler, + undefined, + screenShareOptions, ); if (!onPress) return null; diff --git a/packages/react-native-sdk/src/hooks/index.ts b/packages/react-native-sdk/src/hooks/index.ts index e327538580..deeebd622b 100644 --- a/packages/react-native-sdk/src/hooks/index.ts +++ b/packages/react-native-sdk/src/hooks/index.ts @@ -6,6 +6,7 @@ export * from './useIsIosScreenshareBroadcastStarted'; export * from './useIsInPiPMode'; export * from './useAutoEnterPiPEffect'; export * from './useScreenShareButton'; +export * from './useScreenShareAudioMixing'; export * from './useTrackDimensions'; export * from './useScreenshot'; export * from './useSpeechDetection'; diff --git a/packages/react-native-sdk/src/hooks/useScreenShareAudioMixing.ts b/packages/react-native-sdk/src/hooks/useScreenShareAudioMixing.ts new file mode 100644 index 0000000000..58ebbcbe75 --- /dev/null +++ b/packages/react-native-sdk/src/hooks/useScreenShareAudioMixing.ts @@ -0,0 +1,137 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { hasScreenShare, videoLoggerSystem } from '@stream-io/video-client'; +import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings'; +import { + startScreenShareAudioMixing, + stopScreenShareAudioMixing, +} from '../native/ScreenShareAudioModule'; +import { NoiseCancellationWrapper } from '../providers/NoiseCancellation/lib'; + +const logger = videoLoggerSystem.getLogger('useScreenShareAudioMixing'); + +/** + * Tries to disable noise cancellation so screen audio passes through + * unfiltered. Returns true if NC was disabled (and should be re-enabled later). + */ +async function disableNoiseCancellation(): Promise { + try { + const nc = NoiseCancellationWrapper.getInstance(); + const wasEnabled = await nc.isEnabled(); + if (wasEnabled) { + await nc.disable(); + logger.info('Noise cancellation disabled for screen share audio'); + } + return wasEnabled; + } catch { + // NC module not installed or not configured — nothing to do + return false; + } +} + +/** + * Re-enables noise cancellation if it was previously disabled. + */ +async function restoreNoiseCancellation() { + try { + const nc = NoiseCancellationWrapper.getInstance(); + await nc.enable(); + logger.info('Noise cancellation re-enabled after screen share audio'); + } catch { + // NC module not installed — nothing to do + } +} + +/** + * Hook that manages the lifecycle of screen share audio mixing. + * + * When screen share is active and audio mixing is enabled + * (via `call.screenShare.enableScreenShareAudio()`), this hook + * calls the native module to mix captured screen/app audio + * into the microphone audio track. + * + * Noise cancellation is temporarily disabled while screen audio mixing + * is active so that all captured sounds (music, game audio, etc.) + * pass through without being filtered. + */ +export const useScreenShareAudioMixing = () => { + const call = useCall(); + const { useLocalParticipant } = useCallStateHooks(); + const localParticipant = useLocalParticipant(); + const isScreenSharing = + localParticipant != null && hasScreenShare(localParticipant); + + const [audioEnabled, setAudioEnabled] = useState( + () => call?.screenShare.state.audioEnabled ?? false, + ); + + const isMixingActiveRef = useRef(false); + const ncWasEnabledRef = useRef(false); + + // Subscribe to the audioEnabled state on ScreenShareManager. + // This observable is not exposed by a react-bindings hook, + // so we subscribe to it directly via the call object. + useEffect(() => { + if (!call) return; + const sub = call.screenShare.state.audioEnabled$.subscribe(setAudioEnabled); + return () => sub.unsubscribe(); + }, [call]); + + const startMixing = useCallback(async () => { + if (isMixingActiveRef.current) return; + try { + // Disable NC before starting mixing so screen audio is not filtered + ncWasEnabledRef.current = await disableNoiseCancellation(); + + logger.info('Starting screen share audio mixing'); + await startScreenShareAudioMixing(); + isMixingActiveRef.current = true; + } catch (error) { + logger.warn('Failed to start screen share audio mixing', error); + // Restore NC if we disabled it but mixing failed + if (ncWasEnabledRef.current) { + restoreNoiseCancellation().catch(() => {}); + ncWasEnabledRef.current = false; + } + } + }, []); + + const stopMixing = useCallback(async () => { + if (!isMixingActiveRef.current) return; + try { + logger.info('Stopping screen share audio mixing'); + await stopScreenShareAudioMixing(); + isMixingActiveRef.current = false; + + // Restore NC if we disabled it + if (ncWasEnabledRef.current) { + await restoreNoiseCancellation(); + ncWasEnabledRef.current = false; + } + } catch (error) { + logger.warn('Failed to stop screen share audio mixing', error); + } + }, []); + + // Start/stop audio mixing based on screen share status and audio preference + useEffect(() => { + if (isScreenSharing && audioEnabled) { + startMixing(); + } else { + stopMixing(); + } + }, [isScreenSharing, audioEnabled, startMixing, stopMixing]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (isMixingActiveRef.current) { + stopScreenShareAudioMixing().catch(() => {}); + isMixingActiveRef.current = false; + if (ncWasEnabledRef.current) { + restoreNoiseCancellation().catch(() => {}); + ncWasEnabledRef.current = false; + } + } + }; + }, []); +}; diff --git a/packages/react-native-sdk/src/hooks/useScreenShareButton.ts b/packages/react-native-sdk/src/hooks/useScreenShareButton.ts index b636e017e5..299d3b9e0c 100644 --- a/packages/react-native-sdk/src/hooks/useScreenShareButton.ts +++ b/packages/react-native-sdk/src/hooks/useScreenShareButton.ts @@ -8,6 +8,45 @@ import React, { useEffect, useRef } from 'react'; import { findNodeHandle, NativeModules, Platform } from 'react-native'; import { usePrevious } from '../utils/hooks'; import { useIsIosScreenshareBroadcastStarted } from './useIsIosScreenshareBroadcastStarted'; +import { + startInAppScreenCapture, + stopInAppScreenCapture, +} from '../native/ScreenShareAudioModule'; + +/** + * The type of screen sharing to use on iOS. + * + * - `'broadcast'` — Uses a Broadcast Upload Extension (RPSystemBroadcastPickerView). + * Captures the entire device screen, works across all apps. Requires an extension target. + * - `'inApp'` — Uses RPScreenRecorder.startCapture to capture the current app's screen. + * Only captures the current app. Supports `.audioApp` sample buffers for audio mixing. + * + * On Android, this option is ignored — the system screen capture dialog is always used. + */ +export type ScreenShareType = 'broadcast' | 'inApp'; + +/** + * Options for screen share behavior. + */ +export type ScreenShareOptions = { + /** + * The type of screen sharing on iOS. Default: `'broadcast'`. + * On Android this is ignored. + */ + type?: ScreenShareType; + /** + * Whether to capture and mix system/app audio into the microphone audio track. + * When `true`, remote participants will hear media audio from the shared screen + * (e.g., YouTube video audio) mixed with the user's microphone. + * + * - iOS in-app: Audio captured from RPScreenRecorder `.audioApp` buffers. + * - iOS broadcast: Audio captured from the broadcast extension via socket. + * - Android: Audio captured via AudioPlaybackCaptureConfiguration (API 29+). + * + * Default: `false`. + */ + includeAudio?: boolean; +}; // ios >= 14.0 or android - platform restrictions const CanDeviceScreenShare = @@ -18,7 +57,7 @@ const CanDeviceScreenShare = export const useScreenShareButton = ( /** * Ref of the ScreenCapturePickerView component. - * + * Required for iOS broadcast screen sharing. Can be `null` for in-app mode. */ screenCapturePickerViewiOSRef: React.MutableRefObject, /** @@ -36,6 +75,10 @@ export const useScreenShareButton = ( * */ onMissingScreenShareStreamPermission?: () => void, + /** + * Options for screen share behavior (type, includeAudio). + */ + screenShareOptions?: ScreenShareOptions, ) => { const call = useCall(); const { useLocalParticipant, useCallSettings, useOwnCapabilities } = @@ -47,6 +90,9 @@ export const useScreenShareButton = ( ); const isScreenSharingEnabledInCall = callSettings?.screensharing.enabled; + const screenShareType = screenShareOptions?.type ?? 'broadcast'; + const includeAudio = screenShareOptions?.includeAudio ?? false; + const onScreenShareStartedHandlerRef = useRef(onScreenShareStartedHandler); onScreenShareStartedHandlerRef.current = onScreenShareStartedHandler; const onScreenShareStoppedHandlerRef = useRef(onScreenShareStoppedHandler); @@ -62,15 +108,22 @@ export const useScreenShareButton = ( localParticipant && hasScreenShare(localParticipant); // listens to iOS screen share broadcast started event from the system + // (only relevant for broadcast mode) useEffect(() => { if (Platform.OS !== 'ios') { return; } + if (screenShareType !== 'broadcast') { + return; + } if ( iosScreenShareStartedFromSystem && !prevIosScreenShareStartedFromSystem ) { onScreenShareStartedHandlerRef.current?.(); + if (includeAudio) { + call?.screenShare.enableScreenShareAudio(); + } call?.screenShare.enable(); } else if ( !iosScreenShareStartedFromSystem && @@ -81,6 +134,8 @@ export const useScreenShareButton = ( } }, [ call, + includeAudio, + screenShareType, iosScreenShareStartedFromSystem, prevIosScreenShareStartedFromSystem, ]); @@ -94,12 +149,31 @@ export const useScreenShareButton = ( onMissingScreenShareStreamPermission?.(); } if (!hasPublishedScreenShare) { - if (Platform.OS === 'ios') { + // Set audio mixing preference before starting screen share + if (includeAudio) { + call?.screenShare.enableScreenShareAudio(); + } else { + call?.screenShare.disableScreenShareAudio(); + } + + if (Platform.OS === 'ios' && screenShareType === 'inApp') { + // In-app screen sharing on iOS — uses RPScreenRecorder directly + try { + await startInAppScreenCapture(includeAudio); + await call?.screenShare.enable(); + onScreenShareStartedHandler?.(); + } catch (error) { + const logger = videoLoggerSystem.getLogger('useScreenShareButton'); + logger.warn('Failed to start in-app screen capture', error); + } + } else if (Platform.OS === 'ios') { + // Broadcast screen sharing on iOS — shows the system picker const reactTag = findNodeHandle(screenCapturePickerViewiOSRef.current); await NativeModules.ScreenCapturePickerViewManager.show(reactTag); // After this the iOS screen share broadcast started/stopped event will be triggered // and the useEffect listener will handle the rest } else { + // Android screen sharing try { await call?.screenShare.enable(); onScreenShareStartedHandler?.(); @@ -114,6 +188,10 @@ export const useScreenShareButton = ( } } else if (hasPublishedScreenShare) { onScreenShareStoppedHandler?.(); + // Stop in-app screen capture if it was active (iOS only) + if (Platform.OS === 'ios' && screenShareType === 'inApp') { + await stopInAppScreenCapture(); + } await call?.screenShare.disable(true); } }; diff --git a/packages/react-native-sdk/src/native/ScreenShareAudioModule.ts b/packages/react-native-sdk/src/native/ScreenShareAudioModule.ts new file mode 100644 index 0000000000..e05a503bb6 --- /dev/null +++ b/packages/react-native-sdk/src/native/ScreenShareAudioModule.ts @@ -0,0 +1,47 @@ +import { NativeModules, Platform } from 'react-native'; + +const StreamVideoReactNative = NativeModules.StreamVideoReactNative; + +/** + * Starts mixing screen share audio into the microphone audio track. + * On iOS, this rewires the AVAudioEngine graph to insert a mixer node. + * On Android, this registers an audio processor that captures system media + * audio via AudioPlaybackCaptureConfiguration and mixes it into the mic buffer. + */ +export async function startScreenShareAudioMixing(): Promise { + return StreamVideoReactNative?.startScreenShareAudioMixing(); +} + +/** + * Stops mixing screen share audio into the microphone audio track + * and restores the original audio pipeline. + */ +export async function stopScreenShareAudioMixing(): Promise { + return StreamVideoReactNative?.stopScreenShareAudioMixing(); +} + +/** + * Starts in-app screen capture using RPScreenRecorder (iOS only). + * Unlike broadcast screen sharing, in-app capture runs in the main app process + * and can directly provide `.audioApp` sample buffers for mixing. + * + * @param includeAudio Whether to capture and mix app audio. + */ +export async function startInAppScreenCapture( + includeAudio: boolean, +): Promise { + if (Platform.OS !== 'ios') { + return; + } + return StreamVideoReactNative?.startInAppScreenCapture(includeAudio); +} + +/** + * Stops in-app screen capture (iOS only). + */ +export async function stopInAppScreenCapture(): Promise { + if (Platform.OS !== 'ios') { + return; + } + return StreamVideoReactNative?.stopInAppScreenCapture(); +} diff --git a/packages/react-native-sdk/src/providers/StreamCall/index.tsx b/packages/react-native-sdk/src/providers/StreamCall/index.tsx index 6748eaac21..6798f0a2d8 100644 --- a/packages/react-native-sdk/src/providers/StreamCall/index.tsx +++ b/packages/react-native-sdk/src/providers/StreamCall/index.tsx @@ -4,6 +4,7 @@ import { Call } from '@stream-io/video-client'; import { useIosCallkeepWithCallingStateEffect } from '../../hooks/push/useIosCallkeepWithCallingStateEffect'; import { canAddPushWSSubscriptionsRef } from '../../utils/push/internal/utils'; import { useAndroidKeepCallAliveEffect } from '../../hooks/useAndroidKeepCallAliveEffect'; +import { useScreenShareAudioMixing } from '../../hooks/useScreenShareAudioMixing'; import { AppStateListener } from './AppStateListener'; import { DeviceStats } from './DeviceStats'; import { pushUnsubscriptionCallbacks } from '../../utils/push/internal/constants'; @@ -36,6 +37,7 @@ export const StreamCall = ({ + {children} @@ -60,6 +62,15 @@ const IosInformCallkeepCallEnd = () => { return null; }; +/** + * This is a renderless component to manage screen share audio mixing lifecycle. + * It starts/stops native audio mixing based on screen share status and audio preference. + */ +const ScreenShareAudioMixer = () => { + useScreenShareAudioMixing(); + return null; +}; + /** * This is a renderless component to clear all push ws event subscriptions * and set whether push ws subscriptions can be added or not. diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx index 68a49471af..51c76463c6 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx @@ -50,7 +50,9 @@ export const BottomControls = ({ /> - + Date: Tue, 24 Feb 2026 13:33:25 +0100 Subject: [PATCH 02/12] feat: added in app screen sharing --- .../ios/StreamVideoReactNative.m | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/react-native-sdk/ios/StreamVideoReactNative.m b/packages/react-native-sdk/ios/StreamVideoReactNative.m index ba0b85f511..18d8664a3f 100644 --- a/packages/react-native-sdk/ios/StreamVideoReactNative.m +++ b/packages/react-native-sdk/ios/StreamVideoReactNative.m @@ -626,22 +626,22 @@ - (void)removeAudioInterruptionHandling { - (void)audioSessionInterrupted:(NSNotification *)notification { AVAudioSessionInterruptionType interruptionType = [notification.userInfo[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue]; - + switch (interruptionType) { case AVAudioSessionInterruptionTypeBegan: if (_busyTonePlayer && _busyTonePlayer.isPlaying) { [_busyTonePlayer pause]; } break; - + case AVAudioSessionInterruptionTypeEnded: { AVAudioSessionInterruptionOptions options = [notification.userInfo[AVAudioSessionInterruptionOptionKey] unsignedIntegerValue]; - + if (options & AVAudioSessionInterruptionOptionShouldResume) { // Reactivate audio session NSError *error = nil; [[AVAudioSession sharedInstance] setActive:YES error:&error]; - + if (!error && _busyTonePlayer) { [_busyTonePlayer play]; } else if (error) { @@ -653,4 +653,25 @@ - (void)audioSessionInterrupted:(NSNotification *)notification { } } +#pragma mark - In-App Screen Capture + +RCT_EXPORT_METHOD(startInAppScreenCapture:(BOOL)includeAudio + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance]; + options.useInAppScreenCapture = YES; + options.includeScreenShareAudio = includeAudio; + resolve(nil); +} + +RCT_EXPORT_METHOD(stopInAppScreenCapture:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance]; + options.useInAppScreenCapture = NO; + options.includeScreenShareAudio = NO; + resolve(nil); +} + @end From 5c60bd039c09d8be205218c0709305f2bc6c5531 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Thu, 26 Feb 2026 10:44:02 +0100 Subject: [PATCH 03/12] feat: added ios screen share audio capturing --- .../ios/StreamVideoReactNative.m | 143 ++++++++++++++++++ .../src/hooks/useScreenShareAudioMixing.ts | 14 ++ .../src/native/ScreenShareAudioModule.ts | 30 +++- 3 files changed, 186 insertions(+), 1 deletion(-) diff --git a/packages/react-native-sdk/ios/StreamVideoReactNative.m b/packages/react-native-sdk/ios/StreamVideoReactNative.m index 18d8664a3f..34e814009c 100644 --- a/packages/react-native-sdk/ios/StreamVideoReactNative.m +++ b/packages/react-native-sdk/ios/StreamVideoReactNative.m @@ -7,9 +7,17 @@ #import "StreamVideoReactNative.h" #import "WebRTCModule.h" #import "WebRTCModuleOptions.h" +#import "InAppScreenCapturer.h" #import #import +// Import Swift-generated header for ScreenShareAudioMixer +#if __has_include() +#import +#elif __has_include("stream_react_native_webrtc-Swift.h") +#import "stream_react_native_webrtc-Swift.h" +#endif + // Do not change these consts, it is what is used react-native-webrtc NSNotificationName const kBroadcastStartedNotification = @"iOS_BroadcastStarted"; NSNotificationName const kBroadcastStoppedNotification = @"iOS_BroadcastStopped"; @@ -674,4 +682,139 @@ - (void)audioSessionInterrupted:(NSNotification *)notification { resolve(nil); } +#pragma mark - Screen Share Audio Mixing + +RCT_EXPORT_METHOD(prepareScreenShareAudioMixing:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance]; + + // The mixer is created eagerly at factory init time (in WebRTCModule.m) + // with the delegate set at APM creation. We just verify it exists here. + ScreenShareAudioMixer *mixer = options.screenShareAudioMixer; + if (!mixer) { + // Fallback: create mixer when NC provides its own APM. + // Do NOT set capturePostProcessingDelegate yet — NC may still be active. + // The delegate is set later in startScreenShareAudioMixing (after NC is disabled). + RTCDefaultAudioProcessingModule *apm = options.defaultAudioProcessingModule; + if (!apm) { + reject(@"MIXER_ERROR", @"Neither mixer nor APM available", nil); + return; + } + mixer = [[ScreenShareAudioMixer alloc] init]; + options.screenShareAudioMixer = mixer; + } + + NSLog(@"[StreamVideoReactNative] Screen share audio mixer prepared"); + resolve(nil); +} + +RCT_EXPORT_METHOD(cleanupScreenShareAudioMixing:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance]; + + // Stop mixing if active + ScreenShareAudioMixer *mixer = options.screenShareAudioMixer; + if (mixer) { + [mixer stopMixing]; + } + + // Restore voice processing in case it was left bypassed + WebRTCModule *webrtcModule = [self.bridge moduleForClass:[WebRTCModule class]]; + if (webrtcModule.audioDeviceModule) { + [webrtcModule.audioDeviceModule setVoiceProcessingBypassed:NO]; + } + + // Clear the capture post-processing delegate + RTCDefaultAudioProcessingModule *apm = options.defaultAudioProcessingModule; + if (apm) { + apm.capturePostProcessingDelegate = nil; + } + + // Clear the audio buffer handler on the capturer + InAppScreenCapturer *capturer = options.activeInAppScreenCapturer; + if (capturer) { + capturer.audioBufferHandler = nil; + } + + // Clear the mixer reference + options.screenShareAudioMixer = nil; + + NSLog(@"[StreamVideoReactNative] Screen share audio mixer cleaned up"); + resolve(nil); +} + +RCT_EXPORT_METHOD(startScreenShareAudioMixing:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance]; + + ScreenShareAudioMixer *mixer = options.screenShareAudioMixer; + if (!mixer) { + reject(@"MIXER_ERROR", @"Mixer not prepared — call prepareScreenShareAudioMixing first", nil); + return; + } + + // Ensure mixer is wired as capturePostProcessingDelegate. + // When no custom APM is provided, this was set at factory init time. + // When NC provides its own APM, prepare only created the mixer without + // setting the delegate (to avoid replacing NC's delegate while still active). + // NC should be disabled by now (JS calls disableNoiseCancellation first). + RTCDefaultAudioProcessingModule *apm = options.defaultAudioProcessingModule; + if (apm) { + apm.capturePostProcessingDelegate = mixer; + } + + // Bypass voice processing (AEC/AGC/NS) so screen audio isn't filtered as echo + WebRTCModule *webrtcModule = [self.bridge moduleForClass:[WebRTCModule class]]; + if (webrtcModule.audioDeviceModule) { + [webrtcModule.audioDeviceModule setVoiceProcessingBypassed:YES]; + } + + // Enable audio buffer processing + [mixer startMixing]; + + // Wire audio buffer handler on the active capturer → mixer.enqueue + InAppScreenCapturer *capturer = options.activeInAppScreenCapturer; + if (capturer) { + capturer.audioBufferHandler = ^(CMSampleBufferRef sampleBuffer) { + ScreenShareAudioMixer *currentMixer = [WebRTCModuleOptions sharedInstance].screenShareAudioMixer; + if (currentMixer) { + [currentMixer enqueue:sampleBuffer]; + } + }; + } + + NSLog(@"[StreamVideoReactNative] Screen share audio mixing started (VP bypassed)"); + resolve(nil); +} + +RCT_EXPORT_METHOD(stopScreenShareAudioMixing:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance]; + + // Stop audio buffer processing + ScreenShareAudioMixer *mixer = options.screenShareAudioMixer; + if (mixer) { + [mixer stopMixing]; + } + + // Restore voice processing (AEC/AGC/NS) + WebRTCModule *webrtcModule = [self.bridge moduleForClass:[WebRTCModule class]]; + if (webrtcModule.audioDeviceModule) { + [webrtcModule.audioDeviceModule setVoiceProcessingBypassed:NO]; + } + + // Clear the audio buffer handler on the capturer + InAppScreenCapturer *capturer = options.activeInAppScreenCapturer; + if (capturer) { + capturer.audioBufferHandler = nil; + } + + NSLog(@"[StreamVideoReactNative] Screen share audio mixing stopped (VP restored)"); + resolve(nil); +} + @end diff --git a/packages/react-native-sdk/src/hooks/useScreenShareAudioMixing.ts b/packages/react-native-sdk/src/hooks/useScreenShareAudioMixing.ts index 58ebbcbe75..55fb0ef87b 100644 --- a/packages/react-native-sdk/src/hooks/useScreenShareAudioMixing.ts +++ b/packages/react-native-sdk/src/hooks/useScreenShareAudioMixing.ts @@ -2,6 +2,8 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { hasScreenShare, videoLoggerSystem } from '@stream-io/video-client'; import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings'; import { + cleanupScreenShareAudioMixing, + prepareScreenShareAudioMixing, startScreenShareAudioMixing, stopScreenShareAudioMixing, } from '../native/ScreenShareAudioModule'; @@ -67,6 +69,18 @@ export const useScreenShareAudioMixing = () => { const isMixingActiveRef = useRef(false); const ncWasEnabledRef = useRef(false); + // Prepare the audio mixer early (iOS only) so the audio graph is + // configured during engine setup, before the engine starts rendering. + // This enables safe AVAudioPlayerNode attachment. + useEffect(() => { + prepareScreenShareAudioMixing().catch((e) => + logger.warn('Failed to prepare screen share audio mixing', e), + ); + return () => { + cleanupScreenShareAudioMixing().catch(() => {}); + }; + }, []); + // Subscribe to the audioEnabled state on ScreenShareManager. // This observable is not exposed by a react-bindings hook, // so we subscribe to it directly via the call object. diff --git a/packages/react-native-sdk/src/native/ScreenShareAudioModule.ts b/packages/react-native-sdk/src/native/ScreenShareAudioModule.ts index e05a503bb6..faeba83769 100644 --- a/packages/react-native-sdk/src/native/ScreenShareAudioModule.ts +++ b/packages/react-native-sdk/src/native/ScreenShareAudioModule.ts @@ -2,9 +2,37 @@ import { NativeModules, Platform } from 'react-native'; const StreamVideoReactNative = NativeModules.StreamVideoReactNative; +/** + * Prepares the screen share audio mixer by creating the mixer instance + * and setting it as the audio graph delegate. This should be called early + * (at call join time) so the audio graph is configured during engine setup, + * before the engine starts rendering. + * + * iOS only — Android mixing setup happens inline in start/stop. + */ +export async function prepareScreenShareAudioMixing(): Promise { + if (Platform.OS !== 'ios') { + return; + } + return StreamVideoReactNative?.prepareScreenShareAudioMixing(); +} + +/** + * Cleans up the screen share audio mixer, removing the delegate and + * detaching nodes from the audio graph. Call this when the call ends. + * + * iOS only — Android cleanup happens inline in start/stop. + */ +export async function cleanupScreenShareAudioMixing(): Promise { + if (Platform.OS !== 'ios') { + return; + } + return StreamVideoReactNative?.cleanupScreenShareAudioMixing(); +} + /** * Starts mixing screen share audio into the microphone audio track. - * On iOS, this rewires the AVAudioEngine graph to insert a mixer node. + * On iOS, this enables audio buffer processing on the prepared mixer. * On Android, this registers an audio processor that captures system media * audio via AudioPlaybackCaptureConfiguration and mixes it into the mic buffer. */ From d95572b74f5f9f426eb3bf9d8fdb187415d1bf48 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Wed, 11 Mar 2026 11:21:12 +0100 Subject: [PATCH 04/12] chore: ios moved to mixer node implementation --- .../ios/StreamVideoReactNative.m | 49 +++++-------------- 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/packages/react-native-sdk/ios/StreamVideoReactNative.m b/packages/react-native-sdk/ios/StreamVideoReactNative.m index 34e814009c..b5357f5f64 100644 --- a/packages/react-native-sdk/ios/StreamVideoReactNative.m +++ b/packages/react-native-sdk/ios/StreamVideoReactNative.m @@ -690,22 +690,13 @@ - (void)audioSessionInterrupted:(NSNotification *)notification { WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance]; // The mixer is created eagerly at factory init time (in WebRTCModule.m) - // with the delegate set at APM creation. We just verify it exists here. + // and wired as audioGraphDelegate on the ADM. Just verify it exists. ScreenShareAudioMixer *mixer = options.screenShareAudioMixer; if (!mixer) { - // Fallback: create mixer when NC provides its own APM. - // Do NOT set capturePostProcessingDelegate yet — NC may still be active. - // The delegate is set later in startScreenShareAudioMixing (after NC is disabled). - RTCDefaultAudioProcessingModule *apm = options.defaultAudioProcessingModule; - if (!apm) { - reject(@"MIXER_ERROR", @"Neither mixer nor APM available", nil); - return; - } - mixer = [[ScreenShareAudioMixer alloc] init]; - options.screenShareAudioMixer = mixer; + reject(@"MIXER_ERROR", @"Mixer not available — WebRTCModule may not have initialized", nil); + return; } - NSLog(@"[StreamVideoReactNative] Screen share audio mixer prepared"); resolve(nil); } @@ -726,22 +717,16 @@ - (void)audioSessionInterrupted:(NSNotification *)notification { [webrtcModule.audioDeviceModule setVoiceProcessingBypassed:NO]; } - // Clear the capture post-processing delegate - RTCDefaultAudioProcessingModule *apm = options.defaultAudioProcessingModule; - if (apm) { - apm.capturePostProcessingDelegate = nil; - } - // Clear the audio buffer handler on the capturer InAppScreenCapturer *capturer = options.activeInAppScreenCapturer; if (capturer) { capturer.audioBufferHandler = nil; } - // Clear the mixer reference - options.screenShareAudioMixer = nil; + // NOTE: Do NOT clear options.screenShareAudioMixer — the ADM holds a weak + // reference to it via audioGraphDelegate. Clearing it would deallocate + // the mixer and break future screen share sessions. - NSLog(@"[StreamVideoReactNative] Screen share audio mixer cleaned up"); resolve(nil); } @@ -756,25 +741,19 @@ - (void)audioSessionInterrupted:(NSNotification *)notification { return; } - // Ensure mixer is wired as capturePostProcessingDelegate. - // When no custom APM is provided, this was set at factory init time. - // When NC provides its own APM, prepare only created the mixer without - // setting the delegate (to avoid replacing NC's delegate while still active). - // NC should be disabled by now (JS calls disableNoiseCancellation first). - RTCDefaultAudioProcessingModule *apm = options.defaultAudioProcessingModule; - if (apm) { - apm.capturePostProcessingDelegate = mixer; - } + // Enable mixing FIRST — VP bypass below triggers an engine reconfiguration + // which calls onConfigureInputFromSource. The mixer checks isMixing in that + // callback, so it must be true before the reconfiguration fires. + [mixer startMixing]; - // Bypass voice processing (AEC/AGC/NS) so screen audio isn't filtered as echo + // Bypass voice processing (AEC/AGC/NS) so screen audio isn't filtered as echo. + // This triggers a stop/reconfigure/start cycle, during which onConfigureInputFromSource + // fires and the mixer wires its playerNode + mixerNode into the graph. WebRTCModule *webrtcModule = [self.bridge moduleForClass:[WebRTCModule class]]; if (webrtcModule.audioDeviceModule) { [webrtcModule.audioDeviceModule setVoiceProcessingBypassed:YES]; } - // Enable audio buffer processing - [mixer startMixing]; - // Wire audio buffer handler on the active capturer → mixer.enqueue InAppScreenCapturer *capturer = options.activeInAppScreenCapturer; if (capturer) { @@ -786,7 +765,6 @@ - (void)audioSessionInterrupted:(NSNotification *)notification { }; } - NSLog(@"[StreamVideoReactNative] Screen share audio mixing started (VP bypassed)"); resolve(nil); } @@ -813,7 +791,6 @@ - (void)audioSessionInterrupted:(NSNotification *)notification { capturer.audioBufferHandler = nil; } - NSLog(@"[StreamVideoReactNative] Screen share audio mixing stopped (VP restored)"); resolve(nil); } From 6d7943ababd50ccf97ef5b42c9e9b05f9dbde1bb Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Wed, 18 Mar 2026 14:50:17 +0100 Subject: [PATCH 05/12] chore: extended media usage types for Android --- .../reactnative/screenshare/ScreenAudioCapture.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/screenshare/ScreenAudioCapture.kt b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/screenshare/ScreenAudioCapture.kt index 8b15652e36..df19b0daf3 100644 --- a/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/screenshare/ScreenAudioCapture.kt +++ b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/screenshare/ScreenAudioCapture.kt @@ -15,8 +15,8 @@ import java.nio.ByteBuffer * Captures system media audio using [AudioPlaybackCaptureConfiguration]. * * Uses the given [MediaProjection] to set up an [AudioRecord] that captures - * audio from media playback (e.g., YouTube, music apps) but not notifications - * or system sounds. + * audio from media playback, games, and other apps (USAGE_MEDIA, USAGE_GAME, + * USAGE_UNKNOWN) but not notifications, alarms, or system sounds. * * Audio is captured in a pull-based manner via [getScreenAudioBytes], which * reads exactly the requested number of bytes using [AudioRecord.READ_BLOCKING]. @@ -43,6 +43,8 @@ class ScreenAudioCapture(private val mediaProjection: MediaProjection) { fun start() { val playbackConfig = AudioPlaybackCaptureConfiguration.Builder(mediaProjection) .addMatchingUsage(AudioAttributes.USAGE_MEDIA) + .addMatchingUsage(AudioAttributes.USAGE_GAME) + .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN) .build() val audioFormat = AudioFormat.Builder() From 94319105bac8be151bb7079dc250d9e1ca40343e Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Wed, 18 Mar 2026 15:10:10 +0100 Subject: [PATCH 06/12] chore: code cleanup --- .../StreamVideoReactNativeModule.kt | 18 ++--- .../ios/StreamVideoReactNative.m | 60 +++------------ .../src/hooks/useScreenShareAudioMixing.ts | 33 ++------ .../src/hooks/useScreenShareButton.ts | 13 ++-- .../src/modules/ScreenShareAudioManager.ts | 49 ++++++++++++ .../src/native/ScreenShareAudioModule.ts | 75 ------------------- .../CallControlls/BottomControls.tsx | 2 +- 7 files changed, 76 insertions(+), 174 deletions(-) create mode 100644 packages/react-native-sdk/src/modules/ScreenShareAudioManager.ts delete mode 100644 packages/react-native-sdk/src/native/ScreenShareAudioModule.ts diff --git a/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt index cb1aaff308..a1ac2ed7a3 100644 --- a/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt +++ b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt @@ -506,14 +506,10 @@ class StreamVideoReactNativeModule(reactContext: ReactApplicationContext) : return } - val webRTCModule = reactApplicationContext.getNativeModule(WebRTCModule::class.java) - if (webRTCModule == null) { - promise.reject("WEBRTC_ERROR", "WebRTCModule not found") - return - } + val module = reactApplicationContext.getNativeModule(WebRTCModule::class.java)!! // Get the MediaProjection permission result Intent from WebRTC - val permissionIntent = webRTCModule.userMediaImpl?.mediaProjectionPermissionResultData + val permissionIntent = module.userMediaImpl?.mediaProjectionPermissionResultData if (permissionIntent == null) { promise.reject("NO_PROJECTION", "No MediaProjection permission available. Start screen sharing first.") return @@ -531,17 +527,14 @@ class StreamVideoReactNativeModule(reactContext: ReactApplicationContext) : return } - // Create and start screen audio capture - val capture = ScreenAudioCapture(mediaProjection) - capture.start() - screenAudioCapture = capture + screenAudioCapture = ScreenAudioCapture(mediaProjection).also { it.start() } // Register the screen audio bytes provider so the AudioBufferCallback // in WebRTCModule mixes screen audio into the mic buffer. WebRTCModuleOptions.getInstance().screenAudioBytesProvider = WebRTCModuleOptions.ScreenAudioBytesProvider { bytesRequested -> - capture.getScreenAudioBytes(bytesRequested) - } + screenAudioCapture?.getScreenAudioBytes(bytesRequested) + } Log.d(NAME, "Screen share audio mixing started") promise.resolve(null) @@ -567,7 +560,6 @@ class StreamVideoReactNativeModule(reactContext: ReactApplicationContext) : // Clear the provider so the AudioBufferCallback stops mixing WebRTCModuleOptions.getInstance().screenAudioBytesProvider = null - // Stop and release the audio capture screenAudioCapture?.stop() screenAudioCapture = null diff --git a/packages/react-native-sdk/ios/StreamVideoReactNative.m b/packages/react-native-sdk/ios/StreamVideoReactNative.m index b5357f5f64..bca30dab14 100644 --- a/packages/react-native-sdk/ios/StreamVideoReactNative.m +++ b/packages/react-native-sdk/ios/StreamVideoReactNative.m @@ -46,6 +46,7 @@ @implementation StreamVideoReactNative bool hasListeners; CFNotificationCenterRef _notificationCenter; AVAudioPlayer *_busyTonePlayer; // Instance variable + BOOL _vpBypassedBeforeMixing; // VP bypass state before screen share audio mixing } // necessary for addUIBlock usage https://github.com/facebook/react-native/issues/50800#issuecomment-2823327307 @@ -684,52 +685,6 @@ - (void)audioSessionInterrupted:(NSNotification *)notification { #pragma mark - Screen Share Audio Mixing -RCT_EXPORT_METHOD(prepareScreenShareAudioMixing:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject) -{ - WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance]; - - // The mixer is created eagerly at factory init time (in WebRTCModule.m) - // and wired as audioGraphDelegate on the ADM. Just verify it exists. - ScreenShareAudioMixer *mixer = options.screenShareAudioMixer; - if (!mixer) { - reject(@"MIXER_ERROR", @"Mixer not available — WebRTCModule may not have initialized", nil); - return; - } - - resolve(nil); -} - -RCT_EXPORT_METHOD(cleanupScreenShareAudioMixing:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject) -{ - WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance]; - - // Stop mixing if active - ScreenShareAudioMixer *mixer = options.screenShareAudioMixer; - if (mixer) { - [mixer stopMixing]; - } - - // Restore voice processing in case it was left bypassed - WebRTCModule *webrtcModule = [self.bridge moduleForClass:[WebRTCModule class]]; - if (webrtcModule.audioDeviceModule) { - [webrtcModule.audioDeviceModule setVoiceProcessingBypassed:NO]; - } - - // Clear the audio buffer handler on the capturer - InAppScreenCapturer *capturer = options.activeInAppScreenCapturer; - if (capturer) { - capturer.audioBufferHandler = nil; - } - - // NOTE: Do NOT clear options.screenShareAudioMixer — the ADM holds a weak - // reference to it via audioGraphDelegate. Clearing it would deallocate - // the mixer and break future screen share sessions. - - resolve(nil); -} - RCT_EXPORT_METHOD(startScreenShareAudioMixing:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { @@ -746,11 +701,14 @@ - (void)audioSessionInterrupted:(NSNotification *)notification { // callback, so it must be true before the reconfiguration fires. [mixer startMixing]; - // Bypass voice processing (AEC/AGC/NS) so screen audio isn't filtered as echo. - // This triggers a stop/reconfigure/start cycle, during which onConfigureInputFromSource - // fires and the mixer wires its playerNode + mixerNode into the graph. + // Save current VP bypass state so we can restore it when mixing stops. + // VP may already be bypassed (e.g. stereo playout mode). WebRTCModule *webrtcModule = [self.bridge moduleForClass:[WebRTCModule class]]; if (webrtcModule.audioDeviceModule) { + _vpBypassedBeforeMixing = webrtcModule.audioDeviceModule.isVoiceProcessingBypassed; + // Bypass voice processing (AEC/AGC/NS) so screen audio isn't filtered as echo. + // This triggers a stop/reconfigure/start cycle, during which onConfigureInputFromSource + // fires and the mixer wires its playerNode + mixerNode into the graph. [webrtcModule.audioDeviceModule setVoiceProcessingBypassed:YES]; } @@ -779,10 +737,10 @@ - (void)audioSessionInterrupted:(NSNotification *)notification { [mixer stopMixing]; } - // Restore voice processing (AEC/AGC/NS) + // Restore voice processing bypass to its previous state WebRTCModule *webrtcModule = [self.bridge moduleForClass:[WebRTCModule class]]; if (webrtcModule.audioDeviceModule) { - [webrtcModule.audioDeviceModule setVoiceProcessingBypassed:NO]; + [webrtcModule.audioDeviceModule setVoiceProcessingBypassed:_vpBypassedBeforeMixing]; } // Clear the audio buffer handler on the capturer diff --git a/packages/react-native-sdk/src/hooks/useScreenShareAudioMixing.ts b/packages/react-native-sdk/src/hooks/useScreenShareAudioMixing.ts index 55fb0ef87b..02d2f1bfb2 100644 --- a/packages/react-native-sdk/src/hooks/useScreenShareAudioMixing.ts +++ b/packages/react-native-sdk/src/hooks/useScreenShareAudioMixing.ts @@ -1,12 +1,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { hasScreenShare, videoLoggerSystem } from '@stream-io/video-client'; import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings'; -import { - cleanupScreenShareAudioMixing, - prepareScreenShareAudioMixing, - startScreenShareAudioMixing, - stopScreenShareAudioMixing, -} from '../native/ScreenShareAudioModule'; +import { screenShareAudioMixingManager } from '../modules/ScreenShareAudioManager'; import { NoiseCancellationWrapper } from '../providers/NoiseCancellation/lib'; const logger = videoLoggerSystem.getLogger('useScreenShareAudioMixing'); @@ -69,21 +64,6 @@ export const useScreenShareAudioMixing = () => { const isMixingActiveRef = useRef(false); const ncWasEnabledRef = useRef(false); - // Prepare the audio mixer early (iOS only) so the audio graph is - // configured during engine setup, before the engine starts rendering. - // This enables safe AVAudioPlayerNode attachment. - useEffect(() => { - prepareScreenShareAudioMixing().catch((e) => - logger.warn('Failed to prepare screen share audio mixing', e), - ); - return () => { - cleanupScreenShareAudioMixing().catch(() => {}); - }; - }, []); - - // Subscribe to the audioEnabled state on ScreenShareManager. - // This observable is not exposed by a react-bindings hook, - // so we subscribe to it directly via the call object. useEffect(() => { if (!call) return; const sub = call.screenShare.state.audioEnabled$.subscribe(setAudioEnabled); @@ -97,11 +77,10 @@ export const useScreenShareAudioMixing = () => { ncWasEnabledRef.current = await disableNoiseCancellation(); logger.info('Starting screen share audio mixing'); - await startScreenShareAudioMixing(); + await screenShareAudioMixingManager.startScreenShareAudioMixing(); isMixingActiveRef.current = true; } catch (error) { logger.warn('Failed to start screen share audio mixing', error); - // Restore NC if we disabled it but mixing failed if (ncWasEnabledRef.current) { restoreNoiseCancellation().catch(() => {}); ncWasEnabledRef.current = false; @@ -113,10 +92,9 @@ export const useScreenShareAudioMixing = () => { if (!isMixingActiveRef.current) return; try { logger.info('Stopping screen share audio mixing'); - await stopScreenShareAudioMixing(); + await screenShareAudioMixingManager.stopScreenShareAudioMixing(); isMixingActiveRef.current = false; - // Restore NC if we disabled it if (ncWasEnabledRef.current) { await restoreNoiseCancellation(); ncWasEnabledRef.current = false; @@ -135,11 +113,12 @@ export const useScreenShareAudioMixing = () => { } }, [isScreenSharing, audioEnabled, startMixing, stopMixing]); - // Cleanup on unmount useEffect(() => { return () => { if (isMixingActiveRef.current) { - stopScreenShareAudioMixing().catch(() => {}); + screenShareAudioMixingManager + .stopScreenShareAudioMixing() + .catch(() => {}); isMixingActiveRef.current = false; if (ncWasEnabledRef.current) { restoreNoiseCancellation().catch(() => {}); diff --git a/packages/react-native-sdk/src/hooks/useScreenShareButton.ts b/packages/react-native-sdk/src/hooks/useScreenShareButton.ts index 299d3b9e0c..109af36dbc 100644 --- a/packages/react-native-sdk/src/hooks/useScreenShareButton.ts +++ b/packages/react-native-sdk/src/hooks/useScreenShareButton.ts @@ -8,10 +8,7 @@ import React, { useEffect, useRef } from 'react'; import { findNodeHandle, NativeModules, Platform } from 'react-native'; import { usePrevious } from '../utils/hooks'; import { useIsIosScreenshareBroadcastStarted } from './useIsIosScreenshareBroadcastStarted'; -import { - startInAppScreenCapture, - stopInAppScreenCapture, -} from '../native/ScreenShareAudioModule'; +import { screenShareAudioMixingManager } from '../modules/ScreenShareAudioManager'; /** * The type of screen sharing to use on iOS. @@ -40,7 +37,7 @@ export type ScreenShareOptions = { * (e.g., YouTube video audio) mixed with the user's microphone. * * - iOS in-app: Audio captured from RPScreenRecorder `.audioApp` buffers. - * - iOS broadcast: Audio captured from the broadcast extension via socket. + * - iOS broadcast: Audio mixing is **not** currently supported. * - Android: Audio captured via AudioPlaybackCaptureConfiguration (API 29+). * * Default: `false`. @@ -159,7 +156,9 @@ export const useScreenShareButton = ( if (Platform.OS === 'ios' && screenShareType === 'inApp') { // In-app screen sharing on iOS — uses RPScreenRecorder directly try { - await startInAppScreenCapture(includeAudio); + await screenShareAudioMixingManager.startInAppScreenCapture( + includeAudio, + ); await call?.screenShare.enable(); onScreenShareStartedHandler?.(); } catch (error) { @@ -190,7 +189,7 @@ export const useScreenShareButton = ( onScreenShareStoppedHandler?.(); // Stop in-app screen capture if it was active (iOS only) if (Platform.OS === 'ios' && screenShareType === 'inApp') { - await stopInAppScreenCapture(); + await screenShareAudioMixingManager.stopInAppScreenCapture(); } await call?.screenShare.disable(true); } diff --git a/packages/react-native-sdk/src/modules/ScreenShareAudioManager.ts b/packages/react-native-sdk/src/modules/ScreenShareAudioManager.ts new file mode 100644 index 0000000000..a6ceb79e4a --- /dev/null +++ b/packages/react-native-sdk/src/modules/ScreenShareAudioManager.ts @@ -0,0 +1,49 @@ +import { NativeModules, Platform } from 'react-native'; + +const StreamVideoReactNative = NativeModules.StreamVideoReactNative; + +export class ScreenShareAudioManager { + /** + * Starts mixing screen share audio into the microphone audio track. + * On iOS, this enables audio buffer processing on the prepared mixer. + * On Android, this registers an audio processor that captures system media + * audio via AudioPlaybackCaptureConfiguration and mixes it into the mic buffer. + */ + async startScreenShareAudioMixing(): Promise { + return StreamVideoReactNative?.startScreenShareAudioMixing(); + } + + /** + * Stops mixing screen share audio into the microphone audio track + * and restores the original audio pipeline. + */ + async stopScreenShareAudioMixing(): Promise { + return StreamVideoReactNative?.stopScreenShareAudioMixing(); + } + + /** + * Starts in-app screen capture using RPScreenRecorder (iOS only). + * Unlike broadcast screen sharing, in-app capture runs in the main app process + * and can directly provide `.audioApp` sample buffers for mixing. + * + * @param includeAudio Whether to capture and mix app audio. + */ + async startInAppScreenCapture(includeAudio: boolean): Promise { + if (Platform.OS !== 'ios') { + return; + } + return StreamVideoReactNative?.startInAppScreenCapture(includeAudio); + } + + /** + * Stops in-app screen capture (iOS only). + */ + async stopInAppScreenCapture(): Promise { + if (Platform.OS !== 'ios') { + return; + } + return StreamVideoReactNative?.stopInAppScreenCapture(); + } +} + +export const screenShareAudioMixingManager = new ScreenShareAudioManager(); diff --git a/packages/react-native-sdk/src/native/ScreenShareAudioModule.ts b/packages/react-native-sdk/src/native/ScreenShareAudioModule.ts deleted file mode 100644 index faeba83769..0000000000 --- a/packages/react-native-sdk/src/native/ScreenShareAudioModule.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { NativeModules, Platform } from 'react-native'; - -const StreamVideoReactNative = NativeModules.StreamVideoReactNative; - -/** - * Prepares the screen share audio mixer by creating the mixer instance - * and setting it as the audio graph delegate. This should be called early - * (at call join time) so the audio graph is configured during engine setup, - * before the engine starts rendering. - * - * iOS only — Android mixing setup happens inline in start/stop. - */ -export async function prepareScreenShareAudioMixing(): Promise { - if (Platform.OS !== 'ios') { - return; - } - return StreamVideoReactNative?.prepareScreenShareAudioMixing(); -} - -/** - * Cleans up the screen share audio mixer, removing the delegate and - * detaching nodes from the audio graph. Call this when the call ends. - * - * iOS only — Android cleanup happens inline in start/stop. - */ -export async function cleanupScreenShareAudioMixing(): Promise { - if (Platform.OS !== 'ios') { - return; - } - return StreamVideoReactNative?.cleanupScreenShareAudioMixing(); -} - -/** - * Starts mixing screen share audio into the microphone audio track. - * On iOS, this enables audio buffer processing on the prepared mixer. - * On Android, this registers an audio processor that captures system media - * audio via AudioPlaybackCaptureConfiguration and mixes it into the mic buffer. - */ -export async function startScreenShareAudioMixing(): Promise { - return StreamVideoReactNative?.startScreenShareAudioMixing(); -} - -/** - * Stops mixing screen share audio into the microphone audio track - * and restores the original audio pipeline. - */ -export async function stopScreenShareAudioMixing(): Promise { - return StreamVideoReactNative?.stopScreenShareAudioMixing(); -} - -/** - * Starts in-app screen capture using RPScreenRecorder (iOS only). - * Unlike broadcast screen sharing, in-app capture runs in the main app process - * and can directly provide `.audioApp` sample buffers for mixing. - * - * @param includeAudio Whether to capture and mix app audio. - */ -export async function startInAppScreenCapture( - includeAudio: boolean, -): Promise { - if (Platform.OS !== 'ios') { - return; - } - return StreamVideoReactNative?.startInAppScreenCapture(includeAudio); -} - -/** - * Stops in-app screen capture (iOS only). - */ -export async function stopInAppScreenCapture(): Promise { - if (Platform.OS !== 'ios') { - return; - } - return StreamVideoReactNative?.stopInAppScreenCapture(); -} diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx index 51c76463c6..e7cb40dbdd 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx @@ -51,7 +51,7 @@ export const BottomControls = ({ Date: Mon, 23 Mar 2026 14:59:31 +0100 Subject: [PATCH 07/12] chote: made mixer initialization lazy --- .../ios/StreamVideoReactNative.m | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/packages/react-native-sdk/ios/StreamVideoReactNative.m b/packages/react-native-sdk/ios/StreamVideoReactNative.m index bca30dab14..720dbd2ae4 100644 --- a/packages/react-native-sdk/ios/StreamVideoReactNative.m +++ b/packages/react-native-sdk/ios/StreamVideoReactNative.m @@ -688,35 +688,31 @@ - (void)audioSessionInterrupted:(NSNotification *)notification { RCT_EXPORT_METHOD(startScreenShareAudioMixing:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance]; + WebRTCModule *webrtcModule = [self.bridge moduleForClass:[WebRTCModule class]]; - ScreenShareAudioMixer *mixer = options.screenShareAudioMixer; - if (!mixer) { - reject(@"MIXER_ERROR", @"Mixer not prepared — call prepareScreenShareAudioMixing first", nil); - return; - } + ScreenShareAudioMixer *mixer = [[ScreenShareAudioMixer alloc] init]; + webrtcModule.audioDeviceModule.audioGraphDelegate = mixer; - // Enable mixing FIRST — VP bypass below triggers an engine reconfiguration + // Important: enable mixing first — VP bypass below triggers an engine reconfiguration // which calls onConfigureInputFromSource. The mixer checks isMixing in that // callback, so it must be true before the reconfiguration fires. [mixer startMixing]; - // Save current VP bypass state so we can restore it when mixing stops. - // VP may already be bypassed (e.g. stereo playout mode). - WebRTCModule *webrtcModule = [self.bridge moduleForClass:[WebRTCModule class]]; + // Save current VP bypass state, then bypass voice processing (AEC/AGC/NS) + // so screen audio isn't filtered as echo. This also triggers an engine + // stop/reconfigure/start cycle that fires onConfigureInputFromSource, + // allowing the mixer to wire its nodes into the graph. if (webrtcModule.audioDeviceModule) { _vpBypassedBeforeMixing = webrtcModule.audioDeviceModule.isVoiceProcessingBypassed; - // Bypass voice processing (AEC/AGC/NS) so screen audio isn't filtered as echo. - // This triggers a stop/reconfigure/start cycle, during which onConfigureInputFromSource - // fires and the mixer wires its playerNode + mixerNode into the graph. [webrtcModule.audioDeviceModule setVoiceProcessingBypassed:YES]; } // Wire audio buffer handler on the active capturer → mixer.enqueue + WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance]; InAppScreenCapturer *capturer = options.activeInAppScreenCapturer; if (capturer) { capturer.audioBufferHandler = ^(CMSampleBufferRef sampleBuffer) { - ScreenShareAudioMixer *currentMixer = [WebRTCModuleOptions sharedInstance].screenShareAudioMixer; + ScreenShareAudioMixer *currentMixer = (ScreenShareAudioMixer *)webrtcModule.audioDeviceModule.audioGraphDelegate; if (currentMixer) { [currentMixer enqueue:sampleBuffer]; } @@ -729,21 +725,20 @@ - (void)audioSessionInterrupted:(NSNotification *)notification { RCT_EXPORT_METHOD(stopScreenShareAudioMixing:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance]; + WebRTCModule *webrtcModule = [self.bridge moduleForClass:[WebRTCModule class]]; - // Stop audio buffer processing - ScreenShareAudioMixer *mixer = options.screenShareAudioMixer; + ScreenShareAudioMixer *mixer = (ScreenShareAudioMixer *)webrtcModule.audioDeviceModule.audioGraphDelegate; if (mixer) { [mixer stopMixing]; } + webrtcModule.audioDeviceModule.audioGraphDelegate = nil; // Restore voice processing bypass to its previous state - WebRTCModule *webrtcModule = [self.bridge moduleForClass:[WebRTCModule class]]; if (webrtcModule.audioDeviceModule) { [webrtcModule.audioDeviceModule setVoiceProcessingBypassed:_vpBypassedBeforeMixing]; } - // Clear the audio buffer handler on the capturer + WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance]; InAppScreenCapturer *capturer = options.activeInAppScreenCapturer; if (capturer) { capturer.audioBufferHandler = nil; From 3f6e7392bb871af1bd60f95c2d5426314be8f6ef Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Mon, 23 Mar 2026 18:20:49 +0100 Subject: [PATCH 08/12] chore: audio capture improvement --- .../ios/StreamVideoReactNative.m | 59 +++++++++---------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/packages/react-native-sdk/ios/StreamVideoReactNative.m b/packages/react-native-sdk/ios/StreamVideoReactNative.m index 720dbd2ae4..7dd4fd9988 100644 --- a/packages/react-native-sdk/ios/StreamVideoReactNative.m +++ b/packages/react-native-sdk/ios/StreamVideoReactNative.m @@ -46,7 +46,6 @@ @implementation StreamVideoReactNative bool hasListeners; CFNotificationCenterRef _notificationCenter; AVAudioPlayer *_busyTonePlayer; // Instance variable - BOOL _vpBypassedBeforeMixing; // VP bypass state before screen share audio mixing } // necessary for addUIBlock usage https://github.com/facebook/react-native/issues/50800#issuecomment-2823327307 @@ -689,33 +688,27 @@ - (void)audioSessionInterrupted:(NSNotification *)notification { reject:(RCTPromiseRejectBlock)reject) { WebRTCModule *webrtcModule = [self.bridge moduleForClass:[WebRTCModule class]]; + WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance]; - ScreenShareAudioMixer *mixer = [[ScreenShareAudioMixer alloc] init]; - webrtcModule.audioDeviceModule.audioGraphDelegate = mixer; - - // Important: enable mixing first — VP bypass below triggers an engine reconfiguration - // which calls onConfigureInputFromSource. The mixer checks isMixing in that - // callback, so it must be true before the reconfiguration fires. - [mixer startMixing]; + ScreenShareAudioMixer *mixer = webrtcModule.audioDeviceModule.screenShareAudioMixer; - // Save current VP bypass state, then bypass voice processing (AEC/AGC/NS) - // so screen audio isn't filtered as echo. This also triggers an engine - // stop/reconfigure/start cycle that fires onConfigureInputFromSource, - // allowing the mixer to wire its nodes into the graph. - if (webrtcModule.audioDeviceModule) { - _vpBypassedBeforeMixing = webrtcModule.audioDeviceModule.isVoiceProcessingBypassed; - [webrtcModule.audioDeviceModule setVoiceProcessingBypassed:YES]; + // Wire mixer as capturePostProcessingDelegate on the audio processing module. + id apmId = options.audioProcessingModule; + if (apmId && [apmId isKindOfClass:[RTCDefaultAudioProcessingModule class]]) { + RTCDefaultAudioProcessingModule *apm = (RTCDefaultAudioProcessingModule *)apmId; + apm.capturePostProcessingDelegate = mixer; + NSLog(@"[SSAMixer] Set capturePostProcessingDelegate on APM"); + } else { + NSLog(@"[SSAMixer] WARNING: No RTCDefaultAudioProcessingModule available, mixing will not work"); } + [mixer startMixing]; + // Wire audio buffer handler on the active capturer → mixer.enqueue - WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance]; InAppScreenCapturer *capturer = options.activeInAppScreenCapturer; if (capturer) { capturer.audioBufferHandler = ^(CMSampleBufferRef sampleBuffer) { - ScreenShareAudioMixer *currentMixer = (ScreenShareAudioMixer *)webrtcModule.audioDeviceModule.audioGraphDelegate; - if (currentMixer) { - [currentMixer enqueue:sampleBuffer]; - } + [mixer enqueue:sampleBuffer]; }; } @@ -726,24 +719,26 @@ - (void)audioSessionInterrupted:(NSNotification *)notification { reject:(RCTPromiseRejectBlock)reject) { WebRTCModule *webrtcModule = [self.bridge moduleForClass:[WebRTCModule class]]; - - ScreenShareAudioMixer *mixer = (ScreenShareAudioMixer *)webrtcModule.audioDeviceModule.audioGraphDelegate; - if (mixer) { - [mixer stopMixing]; - } - webrtcModule.audioDeviceModule.audioGraphDelegate = nil; - - // Restore voice processing bypass to its previous state - if (webrtcModule.audioDeviceModule) { - [webrtcModule.audioDeviceModule setVoiceProcessingBypassed:_vpBypassedBeforeMixing]; - } - WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance]; + + // Stop feeding audio to the mixer InAppScreenCapturer *capturer = options.activeInAppScreenCapturer; if (capturer) { capturer.audioBufferHandler = nil; } + // Stop mixing + ScreenShareAudioMixer *mixer = webrtcModule.audioDeviceModule.screenShareAudioMixer; + [mixer stopMixing]; + + // Clear capturePostProcessingDelegate + id apmId = options.audioProcessingModule; + if (apmId && [apmId isKindOfClass:[RTCDefaultAudioProcessingModule class]]) { + RTCDefaultAudioProcessingModule *apm = (RTCDefaultAudioProcessingModule *)apmId; + apm.capturePostProcessingDelegate = nil; + NSLog(@"[SSAMixer] Cleared capturePostProcessingDelegate on APM"); + } + resolve(nil); } From ae0a08d34e0af15935d97fd066980149fb70dd19 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Fri, 27 Mar 2026 15:57:20 +0100 Subject: [PATCH 09/12] chore: added early exit for missing permissions branch --- packages/react-native-sdk/src/hooks/useScreenShareButton.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-native-sdk/src/hooks/useScreenShareButton.ts b/packages/react-native-sdk/src/hooks/useScreenShareButton.ts index 109af36dbc..2bfa676339 100644 --- a/packages/react-native-sdk/src/hooks/useScreenShareButton.ts +++ b/packages/react-native-sdk/src/hooks/useScreenShareButton.ts @@ -144,7 +144,9 @@ export const useScreenShareButton = ( 'User does not have permissions to stream the screen share media, calling onMissingScreenShareStreamPermission handler if present', ); onMissingScreenShareStreamPermission?.(); + return; } + if (!hasPublishedScreenShare) { // Set audio mixing preference before starting screen share if (includeAudio) { From a04243f4ee1e61b0f450b7d2a15f5321622c39e4 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Fri, 27 Mar 2026 16:34:10 +0100 Subject: [PATCH 10/12] chore: pr comment --- packages/react-native-sdk/src/hooks/useScreenShareButton.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native-sdk/src/hooks/useScreenShareButton.ts b/packages/react-native-sdk/src/hooks/useScreenShareButton.ts index 2bfa676339..77d6fdf4dd 100644 --- a/packages/react-native-sdk/src/hooks/useScreenShareButton.ts +++ b/packages/react-native-sdk/src/hooks/useScreenShareButton.ts @@ -164,6 +164,7 @@ export const useScreenShareButton = ( await call?.screenShare.enable(); onScreenShareStartedHandler?.(); } catch (error) { + await screenShareAudioMixingManager.stopInAppScreenCapture(); const logger = videoLoggerSystem.getLogger('useScreenShareButton'); logger.warn('Failed to start in-app screen capture', error); } From b35bbc146ed5ace95ff7a2de2d3324e8f402ef0e Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Fri, 27 Mar 2026 16:55:49 +0100 Subject: [PATCH 11/12] chore: small tweak --- .../react-native-sdk/src/hooks/useScreenShareButton.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/react-native-sdk/src/hooks/useScreenShareButton.ts b/packages/react-native-sdk/src/hooks/useScreenShareButton.ts index 77d6fdf4dd..7fb42f4911 100644 --- a/packages/react-native-sdk/src/hooks/useScreenShareButton.ts +++ b/packages/react-native-sdk/src/hooks/useScreenShareButton.ts @@ -152,7 +152,12 @@ export const useScreenShareButton = ( if (includeAudio) { call?.screenShare.enableScreenShareAudio(); } else { - call?.screenShare.disableScreenShareAudio(); + try { + await call?.screenShare.disableScreenShareAudio(); + } catch (error) { + const logger = videoLoggerSystem.getLogger('useScreenShareButton'); + logger.warn('Failed to disable screen share audio', error); + } } if (Platform.OS === 'ios' && screenShareType === 'inApp') { From 13c1589b96ebaa28606201be7b98a01480948e59 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Mon, 30 Mar 2026 17:06:47 +0200 Subject: [PATCH 12/12] chore: bumped webrtc version --- .../package.json | 2 +- packages/react-native-sdk/package.json | 4 +- .../video-filters-react-native/package.json | 2 +- .../react-native/dogfood/ios/Podfile.lock | 160 +++++++++--------- sample-apps/react-native/dogfood/package.json | 2 +- .../expo-video-sample/package.json | 2 +- .../ringing-tutorial/package.json | 2 +- yarn.lock | 22 +-- 8 files changed, 98 insertions(+), 98 deletions(-) diff --git a/packages/noise-cancellation-react-native/package.json b/packages/noise-cancellation-react-native/package.json index 0403a10ff6..f23edad343 100644 --- a/packages/noise-cancellation-react-native/package.json +++ b/packages/noise-cancellation-react-native/package.json @@ -48,7 +48,7 @@ }, "homepage": "https://github.com/GetStream/stream-video-js#readme", "devDependencies": { - "@stream-io/react-native-webrtc": "137.1.0", + "@stream-io/react-native-webrtc": "137.1.3", "react": "19.1.0", "react-native": "^0.81.5", "react-native-builder-bob": "^0.37.0", diff --git a/packages/react-native-sdk/package.json b/packages/react-native-sdk/package.json index 3842d5116b..4626198caf 100644 --- a/packages/react-native-sdk/package.json +++ b/packages/react-native-sdk/package.json @@ -65,7 +65,7 @@ "@react-native-firebase/app": ">=17.5.0", "@react-native-firebase/messaging": ">=17.5.0", "@stream-io/noise-cancellation-react-native": ">=0.1.0", - "@stream-io/react-native-webrtc": ">=137.1.0", + "@stream-io/react-native-webrtc": ">=137.1.3", "@stream-io/video-filters-react-native": ">=0.1.0", "expo": ">=47.0.0", "expo-build-properties": "*", @@ -131,7 +131,7 @@ "@react-native-firebase/messaging": "^23.4.0", "@react-native/babel-preset": "^0.81.5", "@stream-io/noise-cancellation-react-native": "workspace:^", - "@stream-io/react-native-webrtc": "137.1.0", + "@stream-io/react-native-webrtc": "137.1.3", "@stream-io/video-filters-react-native": "workspace:^", "@testing-library/jest-native": "^5.4.3", "@testing-library/react-native": "13.3.3", diff --git a/packages/video-filters-react-native/package.json b/packages/video-filters-react-native/package.json index bb5b3a55c1..0e7aabb132 100644 --- a/packages/video-filters-react-native/package.json +++ b/packages/video-filters-react-native/package.json @@ -48,7 +48,7 @@ }, "homepage": "https://github.com/GetStream/stream-video-js#readme", "devDependencies": { - "@stream-io/react-native-webrtc": "137.1.0", + "@stream-io/react-native-webrtc": "137.1.3", "react": "19.1.0", "react-native": "^0.81.5", "react-native-builder-bob": "^0.37.0", diff --git a/sample-apps/react-native/dogfood/ios/Podfile.lock b/sample-apps/react-native/dogfood/ios/Podfile.lock index 7a8eb3cc4c..0912967706 100644 --- a/sample-apps/react-native/dogfood/ios/Podfile.lock +++ b/sample-apps/react-native/dogfood/ios/Podfile.lock @@ -3025,7 +3025,7 @@ PODS: - SocketRocket - stream-react-native-webrtc - Yoga - - stream-react-native-webrtc (137.1.0): + - stream-react-native-webrtc (137.1.3): - React-Core - StreamWebRTC (~> 137.0.54) - stream-video-react-native (1.29.3): @@ -3377,98 +3377,98 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 9f4dfe93326146a1c99eb535b1cb0b857a3cd172 - RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 + RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f RCTDeprecation: 5eb1d2eeff5fb91151e8a8eef45b6c7658b6c897 RCTRequired: cebcf9442fc296c9b89ac791dfd463021d9f6f23 RCTTypeSafety: b99aa872829ee18f6e777e0ef55852521c5a6788 React: 914f8695f9bf38e6418228c2ffb70021e559f92f React-callinvoker: 23cd4e33928608bd0cc35357597568b8b9a5f068 React-Codegen: 4b8b4817cea7a54b83851d4c1f91f79aa73de30a - React-Core: 6a0a97598e9455348113bfe4c573fe8edac34469 - React-CoreModules: a88a6ca48b668401b9780e272e2a607e70f9f955 - React-cxxreact: 06265fd7e8d5c3b6b49e00d328ef76e5f1ae9c8b + React-Core: 895a479e2e0331f48af3ad919bece917926a0b7d + React-CoreModules: dfa38212cf3a91f2eb84ccd43d747d006d33449e + React-cxxreact: 7a4e2c77e564792252131e63270f6184d93343b3 React-debug: 29aed758c756956a51b4560223edbd15191ca4c5 - React-defaultsnativemodule: c406bf7cd78036efffb7dec9df469257a1bca58c - React-domnativemodule: 925ea5ff8cb05c68e910057e6349e5898cce00f3 - React-Fabric: 13130d0a70f17e913865b04673ee64603d6c42fe - React-FabricComponents: 1f01ea24a1314bf9abcac4743bb7ad8791336be6 - React-FabricImage: f364dc54fcf8b0ef77192674a009aa4f65b34d75 - React-featureflags: 32217ac18a8c216fc571044186fb04164af72772 - React-featureflagsnativemodule: 9c552bb908a7434baa846002ee1752a77b1a5520 - React-graphics: 3034a698e46e947f74a443e761f1feef742e9d71 - React-hermes: a852be3ab9e1f515e46ba3ea9f48c31d4a9df437 - React-idlecallbacksnativemodule: c43fe1f2221b0548cc366bf15f88efb3b3221bbf - React-ImageManager: 7efd7b19cdfaa3a82482e9e6ac0b56606a3ec271 - React-jserrorhandler: 597057d0b9d158c03e02aa376a4a95f64f46a910 - React-jsi: 7b53959aea60909ac6bbe4dd0bdec6c10d7dc597 - React-jsiexecutor: 19938072af05ade148474bac41e0324a2d733f44 - React-jsinspector: eb6bb244a75cbd56f32767daf2efdb344e2ff10c - React-jsinspectorcdp: 727f37537e9c7ab22b6b86c802d879efae5e2757 - React-jsinspectornetwork: 11d47e644701c58038ef8d7f54a405ddd62b3b16 - React-jsinspectortracing: 8875637e6c65b3b9a3852b006856562e874e7a78 - React-jsitooling: b6e6a2551459a6ef9e1529df2ea981fa27ed3a91 - React-jsitracing: 879e2b2f80dd33d84175989de0a8db5d662505db - React-logger: a913317214a26565cd4c045347edf1bcacb80a3f - React-Mapbuffer: 017336879e2e0fb7537bbc08c24f34e2384c9260 - React-microtasksnativemodule: 63ee6730cec233feab9cdcc0c100dc28a12e4165 - react-native-blob-util: 7f71e0af02279ef38a2ba43e8c2fcb79cf927732 - react-native-image-picker: 6051cfd030121b880a58f1cc0e5e33e9887804e4 - react-native-mmkv: 7b9c7469fa0a7e463f9411ad3e4fe273bd5ff030 - react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187 - react-native-safe-area-context: ee1e8e2a7abf737a8d4d9d1a5686a7f2e7466236 - react-native-video: d9d12aa2325ae06222e97e8bd801bbc31df2675d - React-NativeModulesApple: cbceb3c4cb726838c461b13802a76cefa6f3476f + React-defaultsnativemodule: ee4e3ca63b29c8b91448a6760d72063196ed0796 + React-domnativemodule: 4d29aad12ebb2b5aa34043e5bdd191a92821f3aa + React-Fabric: 21f78a4856240d39a663a52c452e223c5e412098 + React-FabricComponents: 13fc0ac39a488cea00c83ffa7b16113f024d66e6 + React-FabricImage: 8961abe0372d20679ee093d144aaf5fb1227bf41 + React-featureflags: 018934f958e6b83907e71631599b02144e6b17f4 + React-featureflagsnativemodule: 89fef5751203b7d3cdde43e1e10407983735a4b4 + React-graphics: 1c62dd11f47071482ca90238981f0147cce4089d + React-hermes: 36704d7354fff9c9e3fbb2490e8eeb2ac027f6f0 + React-idlecallbacksnativemodule: 5f7cbecc1479b53e665f2cd6c2af2c21a80d2ffd + React-ImageManager: 4cb6318bb2bcc947106e29f9297a1c24c85a9233 + React-jserrorhandler: 4b9344f5794cfe8946f8752d38094649f53dd3f3 + React-jsi: 3a8c6f94a52033b0cca53c38d9bb568306aa9dc1 + React-jsiexecutor: d7cf79b1c2771c6b21c46691a96dd2e32d4454c7 + React-jsinspector: 651483ea1d79859e0ed21b86e9042b2a3f4d2b40 + React-jsinspectorcdp: c800035023789b8bf32b4f5a4c9003c2dc28ee49 + React-jsinspectornetwork: 249ee46e9de930d773ff6e4726aa8eeb5854b589 + React-jsinspectortracing: 80e251e13a6071607f06f0e39e03d3f2ce2645cb + React-jsitooling: 6ce395671d0139ec1c4721953a4d3d92172fc06f + React-jsitracing: 4a4d89334b14d96de0387876751528433d1d2fbd + React-logger: 8bcfaf06f8c536fb9e9760526cf3d17ccd53e4ce + React-Mapbuffer: 4649384414066eb77e30a3124dbb48732a3aa173 + React-microtasksnativemodule: e39f94cc96d61b8174a5cfb2d5862a73fa8c0d35 + react-native-blob-util: 9027c999d7d2b7d4b087ea40413eddc899af46fe + react-native-image-picker: b99d9c1f2ddbef491eb49e1103e5c2ce78b9ab37 + react-native-mmkv: c70c58437c0f5e5fe6992f1de1e350dcdd236332 + react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac + react-native-safe-area-context: 3e0a25a843c40ad5efdf2bef93fdd95ad7229650 + react-native-video: 201e71d168053580319a4741e1ef44380cdcb06f + React-NativeModulesApple: 8ce162c145e6b9767bb37a090c77d3d28f7d32b5 React-oscompat: eb0626e8ba1a2c61673c991bf9dc21834898475d - React-perflogger: 509e1f9a3ee28df71b0a66de806ac515ce951246 - React-performancetimeline: 9ce28cce1cded27410c293283f99fe62bebdb920 + React-perflogger: d0d0d1b884120fa0a13bd38ac5e9c3c8e8bfe82a + React-performancetimeline: ae60fb7a7447c44d4d3227fc4eeba606403aaee3 React-RCTActionSheet: 30fe8f9f8d86db4a25ff34595a658ecd837485fc - React-RCTAnimation: 3126eb1cb8e7a6ca33a52fd833d8018aa9311af1 - React-RCTAppDelegate: b03981c790aa40cf26e0f78cc0f1f2df8287ead4 - React-RCTBlob: 53c35e85c85d6bdaa55dc81a0b290d4e78431095 - React-RCTFabric: 59ad9008775f123019c508efff260594a8509791 - React-RCTFBReactNativeSpec: 82b605ab4f6f8da0a7ad88641161df5a0bafb1fb - React-RCTImage: 074b2faa71a152a456c974e118b60c9eeda94a64 - React-RCTLinking: e5ca17a4f7ae2ad7b0c0483be77e1b383ecd0a8a - React-RCTNetwork: c508d7548c9eceac30a8100a846ea00033a03366 - React-RCTRuntime: 6979568c0bc276fe785e085894f954fa15e0ec7e - React-RCTSettings: dd84c857a4fce42c1e08c1dabcda894e25af4a6e - React-RCTText: 6e4b177d047f98bccb90d6fb1ebdd3391cf8b299 - React-RCTVibration: 9572d4a06a0c92650bcc62913e50eb2a89f19fb6 + React-RCTAnimation: e86dacf8a982f42341a44ec87ea8a30964a15f9f + React-RCTAppDelegate: d7214067e796732b5d22960270593945f1ab8a14 + React-RCTBlob: af1fc227a5aa55564afbe84530a8bd28834fda15 + React-RCTFabric: 8d92e851cc6cdf9771c52a18b144424c92b72082 + React-RCTFBReactNativeSpec: c9ec2130e3c9366d30a85886e1776210054763f5 + React-RCTImage: 70a10a5b957ca124b8d0b0fdeec369f11194782c + React-RCTLinking: 67f8a024192b4844c40ace955c54bb34f40d47f0 + React-RCTNetwork: a7679ee67e7d34797a00cefaa879a3f9ea8cee9c + React-RCTRuntime: 3d25c69970924b597c339aead60168026d7cbc2c + React-RCTSettings: 18d8674195383c4fd51b9fc98ee815b329fba7e4 + React-RCTText: 125574af8e29d0ceb430cbe2a03381d62ba45a47 + React-RCTVibration: e96d43017757197d46834c50a0acfb78429c9b30 React-rendererconsistency: a7b47f8b186af64ff8509c8caec4114a2f1ae63f - React-renderercss: 9845c5063b3a2d0462ed4e4c7fc34219a5d608ed - React-rendererdebug: 3905e346c06347b86c6e49d427062cdd638a3044 - React-RuntimeApple: 97233caf2b635c40819bf5be38d818777f8229ab - React-RuntimeCore: dc41f86fcdf1fbb42a5b8388a29bf59dfa56b2f8 - React-runtimeexecutor: d16d045faaf6cd7de8d1aa8e31a51c13d8db84a4 - React-RuntimeHermes: 5a9d132554c8d6b416d794cd4ac7d927b2f88f7b - React-runtimescheduler: 689d805d43c28b8fb1ab390914e042d10e2ea2ab - React-timing: c39eeb992274aeaeb9f4666dc97a36a31d33fe94 - React-utils: 2f9ba0088251788ad66aa1855ff99ed2424024d2 - ReactAppDependencyProvider: 1bcd3527ac0390a1c898c114f81ff954be35ed79 - ReactCodegen: 2e921a931c5a4dd1d8ab37ade085fdf58fcfe1dd - ReactCommon: 6d0fa86a4510730da7c72560e0ced14258292ab9 - RNCallKeep: 1930a01d8caf48f018be4f2db0c9f03405c2f977 - RNCClipboard: e560338bf6cc4656a09ff90610b62ddc0dbdad65 - RNCPushNotificationIOS: 6c4ca3388c7434e4a662b92e4dfeeee858e6f440 - RNDeviceInfo: bcce8752b5043a623fe3c26789679b473f705d3c - RNGestureHandler: b8d2e75c2e88fc2a1f6be3b3beeeed80b88fa37d - RNNotifee: 5e3b271e8ea7456a36eec994085543c9adca9168 - RNPermissions: ee88ba7d9fb3f9b3650ff7e5a78e7ee2d1c7e200 - RNReactNativeHapticFeedback: 5f1542065f0b24c9252bd8cf3e83bc9c548182e4 - RNReanimated: 44af5b24b999a70c737df8e407582c32d12b3a87 - RNScreens: 2e9c41cd099b1ca50136af8d57c3594214d0086a - RNSVG: 65335d69b8e372837ccad79307b1190d5cc1d0a9 - RNVoipPushNotification: 4998fe6724d421da616dca765da7dc421ff54c4e - RNWorklets: ad0606bee2a8103c14adb412149789c60b72bfb2 + React-renderercss: 0a5b6b7aefc3f5e46a61b0e41b1179a0750cf077 + React-rendererdebug: 7da01af21ab31661c3040ef647e6e2bc55575771 + React-RuntimeApple: 788ca3b8e5485a46654e8a316d4c1e40bf82c5d4 + React-RuntimeCore: 1730e6e5cba6f0af4e0319f891da6426b491e39f + React-runtimeexecutor: 79894322e55268854dc04ff1bee083f24725f6c8 + React-RuntimeHermes: 86bf03cbf11ef05803a2e32087667c8a3cc45f72 + React-runtimescheduler: 70601d598a8a71582fa69a9ba488a27c5d12790d + React-timing: 5717558f0bea206d7557df53015ee9efe1eb57b2 + React-utils: 55e54e497e3d3f373ebfcf844eb77e24ed013356 + ReactAppDependencyProvider: c277c5b231881ad4f00cd59e3aa0671b99d7ebee + ReactCodegen: 1e847cf77c1402fe7394dae41b3829c95569e76e + ReactCommon: 5cfd842fcd893bb40fc835f98fadc60c42906a20 + RNCallKeep: 94bbe46b807ccf58e9f6ec11bc4d6087323a1954 + RNCClipboard: 4eea71d801c880175c12f6ab14332ca86fed935f + RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8 + RNDeviceInfo: 8b6fa8379062949dd79a009cf3d6b02a9c03ca59 + RNGestureHandler: 9339994ea5d1ff6ad2679b7d0cc3d49053111369 + RNNotifee: 4a6ee5c7deaf00e005050052d73ee6315dff7ec9 + RNPermissions: f16d5f721280a171831d992c5886761f08075926 + RNReactNativeHapticFeedback: 43c09eb41d8321be2e1375cb87ae734e58f677b0 + RNReanimated: 91d075aaf0c89d51a0708cd64cd6c77f7fa42cdc + RNScreens: 871305075ddf1291411b051d86da9e0d9289de02 + RNSVG: 596e9946ddc0e023c6e6931038c9e49b553f742d + RNVoipPushNotification: a6f7c09e1ca7220e2be1c45e9b6b897c9600024b + RNWorklets: 582ee5c59370d483a7a3c3f8a8014d48b1bff6bd SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - stream-chat-react-native: 362e74c743dd34d750e1878f9e479707e9edc794 - stream-io-noise-cancellation-react-native: 56787bb94ff912ee17661f4b24a3c4f9551f38ba - stream-io-video-filters-react-native: 8fdd1a1fcade0dcd699fd2e5b61b2152c0056219 - stream-react-native-webrtc: dd4bc6e9717e6d90204008c22a44bc1c1f605e3b - stream-video-react-native: 5be434cde5f0981f5832aa8099bf479bd239eeab + stream-chat-react-native: 97c3894622f2da79e82307d0ac85fe38c121c7fc + stream-io-noise-cancellation-react-native: 1280dabe0c1498d9d5ef70cf5412456d663979f4 + stream-io-video-filters-react-native: d2eae40d043afbecb48c81fe8cf6f9f395fbbd55 + stream-react-native-webrtc: 9a5ebaa1175b8d2f10d590009cb168819a448586 + stream-video-react-native: 328cceb7f42d15aa3faac741f78341533b9841bc StreamVideoNoiseCancellation: 41f5a712aba288f9636b64b17ebfbdff52c61490 StreamWebRTC: 57bd35729bcc46b008de4e741a5b23ac28b8854d - VisionCamera: 891edb31806dd3a239c8a9d6090d6ec78e11ee80 + VisionCamera: 05e4bc4783174689a5878a0797015ab32afae9e4 Yoga: cc4a6600d61e4e9276e860d4d68eebb834a050ba PODFILE CHECKSUM: aa62ba474533b73121c2068a13a8b909b17efbaa diff --git a/sample-apps/react-native/dogfood/package.json b/sample-apps/react-native/dogfood/package.json index 51a5f25bf3..67e1daaaeb 100644 --- a/sample-apps/react-native/dogfood/package.json +++ b/sample-apps/react-native/dogfood/package.json @@ -21,7 +21,7 @@ "@react-navigation/native": "^7.1.18", "@react-navigation/native-stack": "^7.3.27", "@stream-io/noise-cancellation-react-native": "workspace:^", - "@stream-io/react-native-webrtc": "137.1.0", + "@stream-io/react-native-webrtc": "137.1.3", "@stream-io/video-filters-react-native": "workspace:^", "@stream-io/video-react-native-sdk": "workspace:^", "axios": "^1.12.2", diff --git a/sample-apps/react-native/expo-video-sample/package.json b/sample-apps/react-native/expo-video-sample/package.json index 72152cd63a..82f71cce46 100644 --- a/sample-apps/react-native/expo-video-sample/package.json +++ b/sample-apps/react-native/expo-video-sample/package.json @@ -20,7 +20,7 @@ "@react-native-firebase/app": "^23.4.0", "@react-native-firebase/messaging": "^23.4.0", "@stream-io/noise-cancellation-react-native": "workspace:^", - "@stream-io/react-native-webrtc": "137.1.0", + "@stream-io/react-native-webrtc": "137.1.3", "@stream-io/video-filters-react-native": "workspace:^", "@stream-io/video-react-native-sdk": "workspace:^", "expo": "^54.0.12", diff --git a/sample-apps/react-native/ringing-tutorial/package.json b/sample-apps/react-native/ringing-tutorial/package.json index d12c7e3af6..7e041a2ca3 100644 --- a/sample-apps/react-native/ringing-tutorial/package.json +++ b/sample-apps/react-native/ringing-tutorial/package.json @@ -23,7 +23,7 @@ "@react-native-firebase/messaging": "^23.4.0", "@react-navigation/bottom-tabs": "^7.4.8", "@react-navigation/native": "^7.1.18", - "@stream-io/react-native-webrtc": "137.1.0", + "@stream-io/react-native-webrtc": "137.1.3", "@stream-io/video-react-native-sdk": "workspace:^", "expo": "^54.0.12", "expo-blur": "~15.0.7", diff --git a/yarn.lock b/yarn.lock index 9f1701f069..4b5901b2c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7301,7 +7301,7 @@ __metadata: "@rnx-kit/metro-config": "npm:^2.1.2" "@rnx-kit/metro-resolver-symlinks": "npm:^0.2.6" "@stream-io/noise-cancellation-react-native": "workspace:^" - "@stream-io/react-native-webrtc": "npm:137.1.0" + "@stream-io/react-native-webrtc": "npm:137.1.3" "@stream-io/video-filters-react-native": "workspace:^" "@stream-io/video-react-native-sdk": "workspace:^" "@types/react": "npm:~19.1.17" @@ -7406,7 +7406,7 @@ __metadata: version: 0.0.0-use.local resolution: "@stream-io/noise-cancellation-react-native@workspace:packages/noise-cancellation-react-native" dependencies: - "@stream-io/react-native-webrtc": "npm:137.1.0" + "@stream-io/react-native-webrtc": "npm:137.1.3" react: "npm:19.1.0" react-native: "npm:^0.81.5" react-native-builder-bob: "npm:^0.37.0" @@ -7418,16 +7418,16 @@ __metadata: languageName: unknown linkType: soft -"@stream-io/react-native-webrtc@npm:137.1.0": - version: 137.1.0 - resolution: "@stream-io/react-native-webrtc@npm:137.1.0" +"@stream-io/react-native-webrtc@npm:137.1.3": + version: 137.1.3 + resolution: "@stream-io/react-native-webrtc@npm:137.1.3" dependencies: base64-js: "npm:1.5.1" debug: "npm:4.3.4" event-target-shim: "npm:6.0.2" peerDependencies: react-native: ">=0.73.0" - checksum: 10/29f77313c2bce7ebff2d321c2dd07db4c5e9162776cf1f4f97b699b724079801b5cc9d6e9217ce06cfab24249b47d8509c57f6c96990d6dcbc4aaeaabaf149d0 + checksum: 10/9744ea89079a27a3b771ef24f774e24023053cbf8c97fc468abbe609d7341835c025c2452b8dab690294adb891bb96028236c557d794b2b5832336183d488bb3 languageName: node linkType: hard @@ -7517,7 +7517,7 @@ __metadata: version: 0.0.0-use.local resolution: "@stream-io/video-filters-react-native@workspace:packages/video-filters-react-native" dependencies: - "@stream-io/react-native-webrtc": "npm:137.1.0" + "@stream-io/react-native-webrtc": "npm:137.1.3" react: "npm:19.1.0" react-native: "npm:^0.81.5" react-native-builder-bob: "npm:^0.37.0" @@ -7658,7 +7658,7 @@ __metadata: "@rnx-kit/metro-config": "npm:^2.1.2" "@rnx-kit/metro-resolver-symlinks": "npm:^0.2.6" "@stream-io/noise-cancellation-react-native": "workspace:^" - "@stream-io/react-native-webrtc": "npm:137.1.0" + "@stream-io/react-native-webrtc": "npm:137.1.3" "@stream-io/video-filters-react-native": "workspace:^" "@stream-io/video-react-native-sdk": "workspace:^" "@types/react": "npm:~19.1.17" @@ -7710,7 +7710,7 @@ __metadata: "@react-navigation/native": "npm:^7.1.18" "@rnx-kit/metro-config": "npm:^2.1.2" "@rnx-kit/metro-resolver-symlinks": "npm:^0.2.6" - "@stream-io/react-native-webrtc": "npm:137.1.0" + "@stream-io/react-native-webrtc": "npm:137.1.3" "@stream-io/video-react-native-sdk": "workspace:^" "@types/react": "npm:~19.1.17" expo: "npm:^54.0.12" @@ -7759,7 +7759,7 @@ __metadata: "@react-native-firebase/messaging": "npm:^23.4.0" "@react-native/babel-preset": "npm:^0.81.5" "@stream-io/noise-cancellation-react-native": "workspace:^" - "@stream-io/react-native-webrtc": "npm:137.1.0" + "@stream-io/react-native-webrtc": "npm:137.1.3" "@stream-io/video-client": "workspace:*" "@stream-io/video-filters-react-native": "workspace:^" "@stream-io/video-react-bindings": "workspace:*" @@ -7799,7 +7799,7 @@ __metadata: "@react-native-firebase/app": ">=17.5.0" "@react-native-firebase/messaging": ">=17.5.0" "@stream-io/noise-cancellation-react-native": ">=0.1.0" - "@stream-io/react-native-webrtc": ">=137.1.0" + "@stream-io/react-native-webrtc": ">=137.1.3" "@stream-io/video-filters-react-native": ">=0.1.0" expo: ">=47.0.0" expo-build-properties: "*"