diff --git a/packages/noise-cancellation-react-native/package.json b/packages/noise-cancellation-react-native/package.json index 048704e498..238773be24 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/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..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 @@ -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,83 @@ 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 module = reactApplicationContext.getNativeModule(WebRTCModule::class.java)!! + + // Get the MediaProjection permission result Intent from WebRTC + val permissionIntent = module.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 + } + + 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 -> + screenAudioCapture?.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 + + 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..df19b0daf3 --- /dev/null +++ b/packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/screenshare/ScreenAudioCapture.kt @@ -0,0 +1,111 @@ +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, 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]. + * 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) + .addMatchingUsage(AudioAttributes.USAGE_GAME) + .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN) + .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/ios/StreamVideoReactNative.m b/packages/react-native-sdk/ios/StreamVideoReactNative.m index ba0b85f511..7dd4fd9988 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"; @@ -626,22 +634,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 +661,85 @@ - (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); +} + +#pragma mark - Screen Share Audio Mixing + +RCT_EXPORT_METHOD(startScreenShareAudioMixing:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + WebRTCModule *webrtcModule = [self.bridge moduleForClass:[WebRTCModule class]]; + WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance]; + + ScreenShareAudioMixer *mixer = webrtcModule.audioDeviceModule.screenShareAudioMixer; + + // 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 + InAppScreenCapturer *capturer = options.activeInAppScreenCapturer; + if (capturer) { + capturer.audioBufferHandler = ^(CMSampleBufferRef sampleBuffer) { + [mixer enqueue:sampleBuffer]; + }; + } + + resolve(nil); +} + +RCT_EXPORT_METHOD(stopScreenShareAudioMixing:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + WebRTCModule *webrtcModule = [self.bridge moduleForClass:[WebRTCModule class]]; + 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); +} + @end diff --git a/packages/react-native-sdk/package.json b/packages/react-native-sdk/package.json index 805ee42c52..6fabb6751f 100644 --- a/packages/react-native-sdk/package.json +++ b/packages/react-native-sdk/package.json @@ -64,7 +64,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": "*", @@ -130,7 +130,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/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..02d2f1bfb2 --- /dev/null +++ b/packages/react-native-sdk/src/hooks/useScreenShareAudioMixing.ts @@ -0,0 +1,130 @@ +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 { screenShareAudioMixingManager } from '../modules/ScreenShareAudioManager'; +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); + + 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 screenShareAudioMixingManager.startScreenShareAudioMixing(); + isMixingActiveRef.current = true; + } catch (error) { + logger.warn('Failed to start screen share audio mixing', error); + 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 screenShareAudioMixingManager.stopScreenShareAudioMixing(); + isMixingActiveRef.current = false; + + 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]); + + useEffect(() => { + return () => { + if (isMixingActiveRef.current) { + screenShareAudioMixingManager + .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..7fb42f4911 100644 --- a/packages/react-native-sdk/src/hooks/useScreenShareButton.ts +++ b/packages/react-native-sdk/src/hooks/useScreenShareButton.ts @@ -8,6 +8,42 @@ import React, { useEffect, useRef } from 'react'; import { findNodeHandle, NativeModules, Platform } from 'react-native'; import { usePrevious } from '../utils/hooks'; import { useIsIosScreenshareBroadcastStarted } from './useIsIosScreenshareBroadcastStarted'; +import { screenShareAudioMixingManager } from '../modules/ScreenShareAudioManager'; + +/** + * 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 mixing is **not** currently supported. + * - Android: Audio captured via AudioPlaybackCaptureConfiguration (API 29+). + * + * Default: `false`. + */ + includeAudio?: boolean; +}; // ios >= 14.0 or android - platform restrictions const CanDeviceScreenShare = @@ -18,7 +54,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 +72,10 @@ export const useScreenShareButton = ( * */ onMissingScreenShareStreamPermission?: () => void, + /** + * Options for screen share behavior (type, includeAudio). + */ + screenShareOptions?: ScreenShareOptions, ) => { const call = useCall(); const { useLocalParticipant, useCallSettings, useOwnCapabilities } = @@ -47,6 +87,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 +105,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 +131,8 @@ export const useScreenShareButton = ( } }, [ call, + includeAudio, + screenShareType, iosScreenShareStartedFromSystem, prevIosScreenShareStartedFromSystem, ]); @@ -92,14 +144,43 @@ export const useScreenShareButton = ( 'User does not have permissions to stream the screen share media, calling onMissingScreenShareStreamPermission handler if present', ); onMissingScreenShareStreamPermission?.(); + return; } + if (!hasPublishedScreenShare) { - if (Platform.OS === 'ios') { + // Set audio mixing preference before starting screen share + if (includeAudio) { + call?.screenShare.enableScreenShareAudio(); + } else { + 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') { + // In-app screen sharing on iOS — uses RPScreenRecorder directly + try { + await screenShareAudioMixingManager.startInAppScreenCapture( + includeAudio, + ); + 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); + } + } 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 +195,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 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/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/packages/video-filters-react-native/package.json b/packages/video-filters-react-native/package.json index 4c59d447d9..4db201b093 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 a34e812d44..2b79ccf8ff 100644 --- a/sample-apps/react-native/dogfood/ios/Podfile.lock +++ b/sample-apps/react-native/dogfood/ios/Podfile.lock @@ -3162,7 +3162,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - stream-io-noise-cancellation-react-native (0.5.0): + - stream-io-noise-cancellation-react-native (0.5.1): - boost - DoubleConversion - fast_float @@ -3192,7 +3192,7 @@ PODS: - stream-react-native-webrtc - StreamVideoNoiseCancellation - Yoga - - stream-io-video-filters-react-native (0.10.0): + - stream-io-video-filters-react-native (0.10.1): - boost - DoubleConversion - fast_float @@ -3221,10 +3221,10 @@ 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.30.0): + - stream-video-react-native (1.30.5): - boost - DoubleConversion - fast_float @@ -3590,107 +3590,107 @@ SPEC CHECKSUMS: FBLazyVector: f1200e6ef6cf24885501668bdbb9eff4cf48843f fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 - hermes-engine: 79258df51fb2de8c52574d7678c0aeb338e65c3b - RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 + hermes-engine: b9e3cb56773893e8b538b9dad138949344e87269 + RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f RCTDeprecation: 3b915a7b166f7d04eaeb4ae30aaf24236a016551 RCTRequired: fcfec6ba532cfe4e53c49e0a4061b5eff87f8c64 RCTSwiftUI: b2f0c2f2761631b8cd605767536cbb1cbf8d020f - RCTSwiftUIWrapper: 02581172ec5db9b67bce917579130c0f1b564b6f + RCTSwiftUIWrapper: 82b4944db8c3e99e68ff1122e5d39d6c0f4e54de RCTTypeSafety: ef5deb31526e96bee85936b2f9fa9ccf8f009e46 React: f4edc7518ccb0b54a6f580d89dd91471844b4990 React-callinvoker: 55ce59d13846f45dcfb655f03160f54b26b7623e React-Codegen: 4b8b4817cea7a54b83851d4c1f91f79aa73de30a - React-Core: d0a03d911ee2424dda52c8699f32ebba46b51de0 - React-CoreModules: d7fa24a410f5e141720a0eb93584c2f99d49a908 - React-cxxreact: ea34429da15d0cf04dd4489d59c0587af0ae51b4 + React-Core: ca8908221ec94fb099e4aee4b23f12ecb594209f + React-CoreModules: b029cb546324cc89c90668e9269baf15cd581f5a + React-cxxreact: e5d9125d4f584f4a650b80f840911812523921e7 React-debug: ca259fe5f0bafad00e43708897ffbed5ef02ef9b - React-defaultsnativemodule: 9c2b06bc7bc96ef839ea5e6223824a23f6fb3563 - React-domnativemodule: aa3ae71cefef9b70eea96265b665777be5cd9ae8 - React-Fabric: 2a9fbf9e81f8dbf6a5e2267932f2258b4baf6ba3 - React-FabricComponents: 2939605800cc79f64594d975d62c98c3f8aa388c - React-FabricImage: 42f2cc4672722b0e82aba67fa62767b02e78655e - React-featureflags: e3182c52b7b28ded4ef2f3ad37aa1ab0e7ca8501 - React-featureflagsnativemodule: 65102c45496610fd31f422f05d6a4c9699366ea3 - React-graphics: ebbcd053d3fa03c3696989dc376632605dffd126 - React-hermes: 83e56368753e15a50615365ae70eb0871bfa5e1e - React-idlecallbacksnativemodule: 654050622aee6201ea986affc3aa1eb6992e4068 - React-ImageManager: 8e7c1809c41aa3a847ebe4e2827b5f13970febf6 - React-intersectionobservernativemodule: 0fd85ec459b355c195a09a011139c46a977eae10 - React-jserrorhandler: 5e70010ae4a02d47e7e66c8cb8d3359b29e0b6d5 - React-jsi: 8f701a594d95c65b3ca470f8089dfff48e8902bb - React-jsiexecutor: fd6d02d8e5441e358ce652a9271863177871a3a0 - React-jsinspector: f43f0a9400e5e93d1987b73fbcc114c5c3a685ed - React-jsinspectorcdp: 3d5351e81a0bda9f16e797324dff95639daf2e28 - React-jsinspectornetwork: e81eb5ae9152edacdd32368045ff72ea2f705a8e - React-jsinspectortracing: 5824470a5cf2b35024a61decf89864c5cc7771b1 - React-jsitooling: 55aa72c88f4cdcdbd016608b05314358dc763172 - React-jsitracing: cf0a24e9629eda95539206155f6913d73d039205 - React-logger: 49a2029a7c055bf0ef005fec0955c2548fe88f12 - React-Mapbuffer: 341b7d1814f97c607f95b20976423f7b47a84ad7 - React-microtasksnativemodule: 3f5e36e093777ff82ce812f54656697418eb8c08 - 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: b5abec3c13cec8436c802b09cde04fbf70fa3a7b - React-networking: 7868e362186628f01f12674e550933736d96f017 + React-defaultsnativemodule: 9011e763abba216618561d852ce0ce99768879f0 + React-domnativemodule: 3336edad7e606e811c838c1390d9a28a09e0cdb1 + React-Fabric: 850125d934dbf511b0add788b73d7035b29b9d36 + React-FabricComponents: a55b3050b9baf53a24fed0b4926aeee9d79a6b40 + React-FabricImage: 84906cc0c352d14766d96474b4bf3a790c44debe + React-featureflags: 882e16c0c98f3d6a3fe8e3f9d6d541f64b501d4c + React-featureflagsnativemodule: e348dc2dc62e81236c60fca4d23ee6433c13caa8 + React-graphics: b6c2874488663c37f5398c0b78a45bb8acb056a5 + React-hermes: 43d8fc5a7b2dd22f9f1d420cb724daadcbe1fa29 + React-idlecallbacksnativemodule: def8e780bda8b077f7380da10f9173e0204ef191 + React-ImageManager: 45f7686feee3c93ca325b44a9adfa47cff7a1a73 + React-intersectionobservernativemodule: e0247f165505712c9b169c30e8aaebb83153dde2 + React-jserrorhandler: 687870b003cb2c291213ffa459a42edbe9059acd + React-jsi: 3f643b09237167570ad62e8416f61f225072e9fa + React-jsiexecutor: a66875a34dbe62e7345f7fe3d52bc7e52fc7b129 + React-jsinspector: 9ab790d29b86761f6cd01e7c753be834730bf315 + React-jsinspectorcdp: fd58f196154f51f534ae3e565e891cfc26cb5e91 + React-jsinspectornetwork: ebcbf396f7ca378fdc8e4c9fa3079be9a431cc3d + React-jsinspectortracing: c0a4caf5cf2a9169cabfd7886be86daedc30456a + React-jsitooling: fdbb10f6e0698c1451900a35178affb6d1392684 + React-jsitracing: 3dc369824704b3812759b7bf4fd3b9878d8c196e + React-logger: 041882c33e69659747c29ee47399ef3f68666995 + React-Mapbuffer: b7b3fcfcec1007ef3721f1c3facd9a831fdfc9dd + React-microtasksnativemodule: 274d47d4b947c6c5b2245d9b528b3ca6fd0f9940 + 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: 161fa0250bd6e806bd6b3dadbb110ead32ba037a + React-networking: 5ad1e3fcbcc82bfeed5019b85c6142049d995c8d React-oscompat: 173f032a95ee30e92bbd28cd9b4626de0977f828 - React-perflogger: 7575736456f16cb73188e3d96fff7694ec536e46 - React-performancecdpmetrics: 74230f673e06f48c8b0b663ec1e52e71d24eaaec - React-performancetimeline: aa5f62109cfee2a85080c6e28b3647568dfc0134 + React-perflogger: 7fa10f25e97c5d5f49423b9fc9d712332c0b6dac + React-performancecdpmetrics: eeae91cd039d69087ccdcd7a38c852c885811003 + React-performancetimeline: 2de5d800c11ff7d23104914d16581263e80c6df9 React-RCTActionSheet: 238ce94d76d3f43bf8c758520889f1d8bf85f2be - React-RCTAnimation: 5a08881a0720a0e7631244235453657b0462de93 - React-RCTAppDelegate: b6213f1734e62159223579c99e0b3424eb1ec3d4 - React-RCTBlob: 0b1d5e826e3a7f5fb3a4815fee2b8073c9e26e5d - React-RCTFabric: 9202cdde0affea487fb1994478cba1f697e9d38e - React-RCTFBReactNativeSpec: e5b864cc72367db8a2701e663943ebdf16a1cbfe - React-RCTImage: cf5162fe27524c2e607e4c80ea3e944982c9ec00 - React-RCTLinking: f934edb38723114957793330e0bb3187037438df - React-RCTNetwork: 5a28dc4f1f36e3fb4c8e06155da871ef54bada00 - React-RCTRuntime: f580e4e993de23b4d09d16516df6baf1ecf62a78 - React-RCTSettings: 4c064a3f2280ec497f6da494af5c8844bd669cec - React-RCTText: a7ec89373602d43f9ef4224a37dcbf33aa98a4a0 - React-RCTVibration: 05322bcbb94c7dfd0299084532eafb3f35358f06 + React-RCTAnimation: 6c1c7720c97e76a06268b30f37f838c3301be864 + React-RCTAppDelegate: 7f077b0e819f3390ebc702237b62c8f9efb7398d + React-RCTBlob: a6edbed5914706a872e351298d9c00bb809e503f + React-RCTFabric: f35f8fd02c9cd9eea597ccc12498b9708a811ed5 + React-RCTFBReactNativeSpec: 7463e6775893a7363e4fab727523cb760f9ab516 + React-RCTImage: 8309fef87ec397a786a019ea28e5a9c3b330292d + React-RCTLinking: 5c54c58cc2093481768d54c08cc47917b0d1839e + React-RCTNetwork: a3dab6f068f647530858d905893b5afb3a9ad8e4 + React-RCTRuntime: 15cc2e323e5888fb83bf5a15cb090bae1e82198a + React-RCTSettings: a3c63913ea41134a2100c49590bdd94758180e37 + React-RCTText: c8627dda631a3beefc9c68453d52145dbb361329 + React-RCTVibration: 902f5fb272c087d4089c0d5947187c835e17f1a9 React-rendererconsistency: 2c33da8b7ef5fc547a2e6bb6fa8379dfdb961247 - React-renderercss: 684e5478bf3498ca689d04aaf3ff67f8ca1b0101 - React-rendererdebug: 139bb14877c470d335cf095c3d2fb9fb027511aa - React-RuntimeApple: 8d011f676e909919e7393ecadb18787520ae06fc - React-RuntimeCore: 466ffe9ce33e321979d90edfb40ae364d86c7e74 - React-runtimeexecutor: c474e85c37a58becb816ce29e41ee24bba259305 - React-RuntimeHermes: b2eb9cd418e9f842a7c453c7ea85217a6d4bcec5 - React-runtimescheduler: c1130349e02696269391e371fec4e34b241e52d0 - React-timing: 3f3d65478fc7cbb36542e5524d90c307e9b8af49 - React-utils: 4c08b351e05bdbeb1c9a4981b166978154c9d5aa - React-webperformancenativemodule: aba670eb1738056d82222627883e78a438e4d14c - ReactAppDependencyProvider: d69159b417e8c9d82b8fe4d0a27def4c3e8f767f - ReactCodegen: 38a7e572859b574c88edd9f194aee7658033191f - ReactCommon: d86bdfcf07cfd551be3f83831ede1b7680dfd4e7 - RNCallKeep: 1930a01d8caf48f018be4f2db0c9f03405c2f977 - RNCClipboard: e560338bf6cc4656a09ff90610b62ddc0dbdad65 - RNCPushNotificationIOS: 6c4ca3388c7434e4a662b92e4dfeeee858e6f440 - RNDeviceInfo: bcce8752b5043a623fe3c26789679b473f705d3c - RNGestureHandler: 4db26426b6098e3d87bd26d8c9a9afa3b6674123 - RNNotifee: 5e3b271e8ea7456a36eec994085543c9adca9168 - RNPermissions: ee88ba7d9fb3f9b3650ff7e5a78e7ee2d1c7e200 - RNReactNativeHapticFeedback: 5f1542065f0b24c9252bd8cf3e83bc9c548182e4 - RNReanimated: fbcb7fd8da5b0b088401542c58fb5d266388f1cf - RNScreens: 2e9c41cd099b1ca50136af8d57c3594214d0086a - RNSVG: d9938e7f6ea1616124dd2d64b933322ac2059e05 - RNVoipPushNotification: 4998fe6724d421da616dca765da7dc421ff54c4e - RNWorklets: 944dddd0eef13006b658e653abbb3ee8365c3809 + React-renderercss: 831bc3c54a475a8a7dc40aa1214d72066990768d + React-rendererdebug: 2b14ecbabe7050e0dddf35486ccf5a244738fd8b + React-RuntimeApple: 679e1177ee6e811e2fc959a1131db7356aa8ffbe + React-RuntimeCore: bc1f5c87af5c4bee955c12d71146f97b801e8ddb + React-runtimeexecutor: 47c8f4101beafa54b08abd27feb23e8c25886a06 + React-RuntimeHermes: 4787419e6512bda027700a6987c1590ff83124c3 + React-runtimescheduler: 2d3e07d8de2e5242ebb3189bd2edd90320554a33 + React-timing: 8a7692de41c7a1e152d02ca99e7daf38817b1a32 + React-utils: 95eedd35726a7a91feb374ce5df2e9ed2402fa1a + React-webperformancenativemodule: 495d6c52b3deac86b2a074c288290073ef2d90bb + ReactAppDependencyProvider: 1976cdf5076a7e34718a56ead2f2069c7f54ebe9 + ReactCodegen: 5a5391bff894d39f4f110c9d2c4f31a01953581f + ReactCommon: 69b84bf292e29f218e77631304161c0a1841dd82 + RNCallKeep: 94bbe46b807ccf58e9f6ec11bc4d6087323a1954 + RNCClipboard: 4eea71d801c880175c12f6ab14332ca86fed935f + RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8 + RNDeviceInfo: 8b6fa8379062949dd79a009cf3d6b02a9c03ca59 + RNGestureHandler: 9f2af339dd736c7b2de7d0df5256af69b5f25cef + RNNotifee: 4a6ee5c7deaf00e005050052d73ee6315dff7ec9 + RNPermissions: f16d5f721280a171831d992c5886761f08075926 + RNReactNativeHapticFeedback: 43c09eb41d8321be2e1375cb87ae734e58f677b0 + RNReanimated: 0a23a0a9721f450580f0fdf04b9ae928cd22915a + RNScreens: 871305075ddf1291411b051d86da9e0d9289de02 + RNSVG: 14a9b979bfb8fc5b90c03de67409f399f847b3b3 + RNVoipPushNotification: a6f7c09e1ca7220e2be1c45e9b6b897c9600024b + RNWorklets: b5f7871d7544b3af3a0bbf2187733e989e426e8b 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: 68b9318fd73565de75ca25f35922b4c843d0227b + stream-chat-react-native: 97c3894622f2da79e82307d0ac85fe38c121c7fc + stream-io-noise-cancellation-react-native: e030ba6739f33114f5efc8d0fe1fa509d9159bc1 + stream-io-video-filters-react-native: 7c4f1651b06adb4089fa421ebf14ba2a9be1545b + stream-react-native-webrtc: 9a5ebaa1175b8d2f10d590009cb168819a448586 + stream-video-react-native: fcbe50aff06854b202cc4fdaa4997d65d89871e2 StreamVideoNoiseCancellation: 41f5a712aba288f9636b64b17ebfbdff52c61490 StreamWebRTC: 57bd35729bcc46b008de4e741a5b23ac28b8854d - VisionCamera: 891edb31806dd3a239c8a9d6090d6ec78e11ee80 + VisionCamera: 05e4bc4783174689a5878a0797015ab32afae9e4 Yoga: 89dfd64939cad2d0d2cc2dc48193100cef28bd98 PODFILE CHECKSUM: 40ce52e159cfdb7fd419e1127f09742de1db1cd5 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/sample-apps/react-native/dogfood/package.json b/sample-apps/react-native/dogfood/package.json index 5b024083f8..e68f5b5e9e 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/dogfood/src/components/CallControlls/BottomControls.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx index 68a49471af..e7cb40dbdd 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 = ({ /> - + =0.73.0" - checksum: 10/29f77313c2bce7ebff2d321c2dd07db4c5e9162776cf1f4f97b699b724079801b5cc9d6e9217ce06cfab24249b47d8509c57f6c96990d6dcbc4aaeaabaf149d0 + checksum: 10/9744ea89079a27a3b771ef24f774e24023053cbf8c97fc468abbe609d7341835c025c2452b8dab690294adb891bb96028236c557d794b2b5832336183d488bb3 languageName: node linkType: hard @@ -7886,7 +7886,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" @@ -8027,7 +8027,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.2.0" @@ -8079,7 +8079,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" @@ -8128,7 +8128,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:*" @@ -8166,7 +8166,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: "*"