@stream-io/video-react-native-sdk 1.42.0 → 1.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +41 -6
  2. package/dist/commonjs/components/Call/CallControls/internal/ReactionsPicker.js.map +1 -1
  3. package/dist/commonjs/components/Call/Lobby/Lobby.js +4 -9
  4. package/dist/commonjs/components/Call/Lobby/Lobby.js.map +1 -1
  5. package/dist/commonjs/components/Call/Lobby/LobbyCameraPreview.js +93 -0
  6. package/dist/commonjs/components/Call/Lobby/LobbyCameraPreview.js.map +1 -0
  7. package/dist/commonjs/components/Call/Lobby/index.js +11 -0
  8. package/dist/commonjs/components/Call/Lobby/index.js.map +1 -1
  9. package/dist/commonjs/components/Call/RingingCallContent/OutgoingCall.js.map +1 -1
  10. package/dist/commonjs/components/Participant/FloatingParticipantView/index.js.map +1 -1
  11. package/dist/commonjs/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.js +4 -15
  12. package/dist/commonjs/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.js.map +1 -1
  13. package/dist/commonjs/components/Participant/ParticipantView/VideoRenderer/index.js +10 -4
  14. package/dist/commonjs/components/Participant/ParticipantView/VideoRenderer/index.js.map +1 -1
  15. package/dist/commonjs/contexts/ThemeContext.js +10 -0
  16. package/dist/commonjs/contexts/ThemeContext.js.map +1 -1
  17. package/dist/commonjs/hooks/useScreenShareButton.js.map +1 -1
  18. package/dist/commonjs/modules/call-manager/CallManager.js +37 -54
  19. package/dist/commonjs/modules/call-manager/CallManager.js.map +1 -1
  20. package/dist/commonjs/providers/NoiseCancellation/lib.js.map +1 -1
  21. package/dist/commonjs/providers/StreamCall/AudioInterruptionTracer.js.map +1 -1
  22. package/dist/commonjs/utils/internal/callingx/callingx.js +26 -37
  23. package/dist/commonjs/utils/internal/callingx/callingx.js.map +1 -1
  24. package/dist/commonjs/utils/internal/registerMediaEngine.js +41 -0
  25. package/dist/commonjs/utils/internal/registerMediaEngine.js.map +1 -0
  26. package/dist/commonjs/utils/internal/registerSDKGlobals.js +93 -35
  27. package/dist/commonjs/utils/internal/registerSDKGlobals.js.map +1 -1
  28. package/dist/commonjs/utils/push/android.js +6 -8
  29. package/dist/commonjs/utils/push/android.js.map +1 -1
  30. package/dist/commonjs/utils/push/libs/firebaseMessaging/index.js +0 -9
  31. package/dist/commonjs/utils/push/libs/firebaseMessaging/index.js.map +1 -1
  32. package/dist/commonjs/version.js +1 -1
  33. package/dist/module/components/Call/CallControls/internal/ReactionsPicker.js.map +1 -1
  34. package/dist/module/components/Call/Lobby/Lobby.js +4 -9
  35. package/dist/module/components/Call/Lobby/Lobby.js.map +1 -1
  36. package/dist/module/components/Call/Lobby/LobbyCameraPreview.js +87 -0
  37. package/dist/module/components/Call/Lobby/LobbyCameraPreview.js.map +1 -0
  38. package/dist/module/components/Call/Lobby/index.js +1 -0
  39. package/dist/module/components/Call/Lobby/index.js.map +1 -1
  40. package/dist/module/components/Call/RingingCallContent/OutgoingCall.js.map +1 -1
  41. package/dist/module/components/Participant/FloatingParticipantView/index.js.map +1 -1
  42. package/dist/module/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.js +6 -17
  43. package/dist/module/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.js.map +1 -1
  44. package/dist/module/components/Participant/ParticipantView/VideoRenderer/index.js +11 -5
  45. package/dist/module/components/Participant/ParticipantView/VideoRenderer/index.js.map +1 -1
  46. package/dist/module/contexts/ThemeContext.js +10 -0
  47. package/dist/module/contexts/ThemeContext.js.map +1 -1
  48. package/dist/module/hooks/useScreenShareButton.js.map +1 -1
  49. package/dist/module/modules/call-manager/CallManager.js +36 -53
  50. package/dist/module/modules/call-manager/CallManager.js.map +1 -1
  51. package/dist/module/providers/NoiseCancellation/lib.js.map +1 -1
  52. package/dist/module/providers/StreamCall/AudioInterruptionTracer.js.map +1 -1
  53. package/dist/module/utils/internal/callingx/callingx.js +25 -38
  54. package/dist/module/utils/internal/callingx/callingx.js.map +1 -1
  55. package/dist/module/utils/internal/registerMediaEngine.js +37 -0
  56. package/dist/module/utils/internal/registerMediaEngine.js.map +1 -0
  57. package/dist/module/utils/internal/registerSDKGlobals.js +95 -37
  58. package/dist/module/utils/internal/registerSDKGlobals.js.map +1 -1
  59. package/dist/module/utils/push/android.js +7 -9
  60. package/dist/module/utils/push/android.js.map +1 -1
  61. package/dist/module/utils/push/libs/firebaseMessaging/index.js +0 -8
  62. package/dist/module/utils/push/libs/firebaseMessaging/index.js.map +1 -1
  63. package/dist/module/version.js +1 -1
  64. package/dist/typescript/components/Call/Lobby/Lobby.d.ts.map +1 -1
  65. package/dist/typescript/components/Call/Lobby/LobbyCameraPreview.d.ts +28 -0
  66. package/dist/typescript/components/Call/Lobby/LobbyCameraPreview.d.ts.map +1 -0
  67. package/dist/typescript/components/Call/Lobby/index.d.ts +1 -0
  68. package/dist/typescript/components/Call/Lobby/index.d.ts.map +1 -1
  69. package/dist/typescript/components/Participant/FloatingParticipantView/index.d.ts.map +1 -1
  70. package/dist/typescript/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.d.ts +11 -6
  71. package/dist/typescript/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.d.ts.map +1 -1
  72. package/dist/typescript/components/Participant/ParticipantView/VideoRenderer/index.d.ts.map +1 -1
  73. package/dist/typescript/contexts/ThemeContext.d.ts +10 -1
  74. package/dist/typescript/contexts/ThemeContext.d.ts.map +1 -1
  75. package/dist/typescript/hooks/useScreenShareButton.d.ts +2 -1
  76. package/dist/typescript/hooks/useScreenShareButton.d.ts.map +1 -1
  77. package/dist/typescript/modules/call-manager/CallManager.d.ts +28 -6
  78. package/dist/typescript/modules/call-manager/CallManager.d.ts.map +1 -1
  79. package/dist/typescript/modules/call-manager/types.d.ts +0 -1
  80. package/dist/typescript/modules/call-manager/types.d.ts.map +1 -1
  81. package/dist/typescript/providers/NoiseCancellation/lib.d.ts.map +1 -1
  82. package/dist/typescript/providers/StreamCall/AudioInterruptionTracer.d.ts.map +1 -1
  83. package/dist/typescript/utils/StreamVideoRN/types.d.ts +3 -0
  84. package/dist/typescript/utils/StreamVideoRN/types.d.ts.map +1 -1
  85. package/dist/typescript/utils/internal/callingx/callingx.d.ts +2 -0
  86. package/dist/typescript/utils/internal/callingx/callingx.d.ts.map +1 -1
  87. package/dist/typescript/utils/internal/registerMediaEngine.d.ts +15 -0
  88. package/dist/typescript/utils/internal/registerMediaEngine.d.ts.map +1 -0
  89. package/dist/typescript/utils/internal/registerSDKGlobals.d.ts.map +1 -1
  90. package/dist/typescript/utils/push/android.d.ts.map +1 -1
  91. package/dist/typescript/utils/push/libs/firebaseMessaging/index.d.ts +0 -1
  92. package/dist/typescript/utils/push/libs/firebaseMessaging/index.d.ts.map +1 -1
  93. package/dist/typescript/version.d.ts +1 -1
  94. package/expo-config-plugin/dist/withAndroidMessagingService.js +29 -3
  95. package/expo-config-plugin/dist/withIosScreenCapture/addBroadcastExtensionXcodeTarget.js +84 -2
  96. package/expo-config-plugin/dist/withIosScreenCapture/withPlistUpdates.js +3 -6
  97. package/expo-config-plugin/dist/withIosScreenCapture/withTarget.js +6 -4
  98. package/expo-config-plugin/static/AudioUploader.swift +230 -0
  99. package/expo-config-plugin/static/SampleHandler.swift +67 -14
  100. package/ios/StreamInCallManager.m +4 -0
  101. package/ios/StreamInCallManager.swift +44 -12
  102. package/ios/StreamVideoReactNative.m +15 -5
  103. package/package.json +27 -25
  104. package/src/components/Call/CallControls/internal/ReactionsPicker.tsx +2 -2
  105. package/src/components/Call/Lobby/Lobby.tsx +4 -11
  106. package/src/components/Call/Lobby/LobbyCameraPreview.tsx +99 -0
  107. package/src/components/Call/Lobby/index.ts +1 -0
  108. package/src/components/Call/RingingCallContent/OutgoingCall.tsx +1 -2
  109. package/src/components/Participant/FloatingParticipantView/index.tsx +1 -4
  110. package/src/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.tsx +56 -75
  111. package/src/components/Participant/ParticipantView/VideoRenderer/index.tsx +18 -7
  112. package/src/contexts/ThemeContext.tsx +10 -1
  113. package/src/hooks/useScreenShareButton.ts +2 -1
  114. package/src/modules/call-manager/CallManager.ts +41 -73
  115. package/src/modules/call-manager/native-module.d.ts +13 -0
  116. package/src/modules/call-manager/types.ts +0 -1
  117. package/src/providers/NoiseCancellation/lib.ts +1 -2
  118. package/src/providers/StreamCall/AudioInterruptionTracer.tsx +1 -2
  119. package/src/utils/StreamVideoRN/types.ts +4 -3
  120. package/src/utils/internal/callingx/callingx.ts +35 -44
  121. package/src/utils/internal/registerMediaEngine.ts +42 -0
  122. package/src/utils/internal/registerSDKGlobals.ts +115 -42
  123. package/src/utils/push/android.ts +9 -17
  124. package/src/utils/push/libs/firebaseMessaging/index.ts +0 -19
  125. package/src/version.ts +1 -1
@@ -2,6 +2,7 @@
2
2
  // SampleHandler.swift
3
3
  // Broadcast Extension
4
4
  //
5
+
5
6
  import ReplayKit
6
7
  import OSLog
7
8
 
@@ -9,26 +10,47 @@ let broadcastLogger = OSLog(subsystem: "io.getstream.reactnative", category: "Br
9
10
  private enum Constants {
10
11
  // the App Group ID value that the app and the broadcast extension targets are setup with. It differs for each app.
11
12
  static let appGroupIdentifier = "group.com.example.broadcast.appgroup"
13
+ static let videoSocketName = "rtc_SSFD"
14
+ static let audioSocketName = "rtc_SSFD_audio"
15
+ static let audioConnectMaxAttempts = 300
12
16
  }
13
17
  class SampleHandler: RPBroadcastSampleHandler {
14
18
 
15
- private var clientConnection: SocketConnection?
16
- private var uploader: SampleUploader?
19
+ private var videoClientConnection: SocketConnection?
20
+ private var videoUploader: SampleUploader?
21
+
22
+ private var audioClientConnection: SocketConnection?
23
+ private var audioUploader: AudioUploader?
17
24
 
18
25
  private var frameCount: Int = 0
19
26
 
27
+ private func socketFilePath(for name: String) -> String {
28
+ let sharedContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupIdentifier)
29
+ return sharedContainer?.appendingPathComponent(name).path ?? ""
30
+ }
31
+
20
32
  var socketFilePath: String {
21
- let sharedContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupIdentifier)
22
- return sharedContainer?.appendingPathComponent("rtc_SSFD").path ?? ""
33
+ socketFilePath(for: Constants.videoSocketName)
34
+ }
35
+
36
+ var audioSocketFilePath: String {
37
+ socketFilePath(for: Constants.audioSocketName)
23
38
  }
24
39
 
25
40
  override init() {
26
41
  super.init()
27
42
  if let connection = SocketConnection(filePath: socketFilePath) {
28
- clientConnection = connection
29
- setupConnection()
43
+ videoClientConnection = connection
44
+ setupVideoConnection()
30
45
 
31
- uploader = SampleUploader(connection: connection)
46
+ videoUploader = SampleUploader(connection: connection)
47
+ }
48
+
49
+ if let connection = SocketConnection(filePath: audioSocketFilePath) {
50
+ audioClientConnection = connection
51
+ setupAudioConnection()
52
+
53
+ audioUploader = AudioUploader(connection: connection)
32
54
  }
33
55
  os_log(.debug, log: broadcastLogger, "%{public}s", socketFilePath)
34
56
  }
@@ -38,7 +60,8 @@ class SampleHandler: RPBroadcastSampleHandler {
38
60
  frameCount = 0
39
61
 
40
62
  DarwinNotificationCenter.shared.postNotification(.broadcastStarted)
41
- openConnection()
63
+ openVideoConnection()
64
+ openAudioConnection()
42
65
  }
43
66
 
44
67
  override func broadcastPaused() {
@@ -52,14 +75,18 @@ class SampleHandler: RPBroadcastSampleHandler {
52
75
  override func broadcastFinished() {
53
76
  // User has requested to finish the broadcast.
54
77
  DarwinNotificationCenter.shared.postNotification(.broadcastStopped)
55
- clientConnection?.close()
78
+ videoClientConnection?.close()
79
+ audioClientConnection?.close()
56
80
  }
57
81
 
58
82
  override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
59
83
  switch sampleBufferType {
60
84
  case RPSampleBufferType.video:
61
- uploader?.send(sample: sampleBuffer)
85
+ videoUploader?.send(sample: sampleBuffer)
86
+ case RPSampleBufferType.audioApp:
87
+ audioUploader?.send(sample: sampleBuffer)
62
88
  default:
89
+ // .audioMic is handled by the app's audio device module; ignore it here.
63
90
  break
64
91
  }
65
92
  }
@@ -67,8 +94,8 @@ class SampleHandler: RPBroadcastSampleHandler {
67
94
 
68
95
  private extension SampleHandler {
69
96
 
70
- func setupConnection() {
71
- clientConnection?.didClose = { [weak self] error in
97
+ func setupVideoConnection() {
98
+ videoClientConnection?.didClose = { [weak self] error in
72
99
  os_log(.debug, log: broadcastLogger, "client connection did close \(String(describing: error))")
73
100
 
74
101
  if let error = error {
@@ -82,12 +109,19 @@ private extension SampleHandler {
82
109
  }
83
110
  }
84
111
 
85
- func openConnection() {
112
+ func setupAudioConnection() {
113
+ // closed audio socket must not tear down the whole broadcast.
114
+ audioClientConnection?.didClose = { error in
115
+ os_log(.debug, log: broadcastLogger, "audio connection did close \(String(describing: error))")
116
+ }
117
+ }
118
+
119
+ func openVideoConnection() {
86
120
  let queue = DispatchQueue(label: "broadcast.connectTimer")
87
121
  let timer = DispatchSource.makeTimerSource(queue: queue)
88
122
  timer.schedule(deadline: .now(), repeating: .milliseconds(100), leeway: .milliseconds(500))
89
123
  timer.setEventHandler { [weak self] in
90
- guard self?.clientConnection?.open() == true else {
124
+ guard self?.videoClientConnection?.open() == true else {
91
125
  return
92
126
  }
93
127
 
@@ -96,4 +130,23 @@ private extension SampleHandler {
96
130
 
97
131
  timer.resume()
98
132
  }
133
+
134
+ func openAudioConnection() {
135
+ let queue = DispatchQueue(label: "broadcast.audioConnectTimer")
136
+ let timer = DispatchSource.makeTimerSource(queue: queue)
137
+ timer.schedule(deadline: .now(), repeating: .milliseconds(100), leeway: .milliseconds(500))
138
+ var attempts = 0
139
+ timer.setEventHandler { [weak self] in
140
+ attempts += 1
141
+ if self?.audioClientConnection?.open() == true {
142
+ timer.cancel()
143
+ return
144
+ }
145
+ if attempts >= Constants.audioConnectMaxAttempts {
146
+ timer.cancel()
147
+ }
148
+ }
149
+
150
+ timer.resume()
151
+ }
99
152
  }
@@ -9,6 +9,10 @@ RCT_EXTERN_METHOD(setDefaultAudioDeviceEndpointType:(NSString *)endpointType)
9
9
 
10
10
  RCT_EXTERN_METHOD(setEnableStereoAudioOutput:(BOOL)enable)
11
11
 
12
+ RCT_EXTERN_METHOD(setMuteMode:(NSInteger)mode)
13
+
14
+ RCT_EXTERN_METHOD(setRecordingAlwaysPreparedMode:(BOOL)enabled)
15
+
12
16
  RCT_EXTERN_METHOD(setup)
13
17
 
14
18
  RCT_EXTERN_METHOD(start)
@@ -124,7 +124,30 @@ class StreamInCallManager: RCTEventEmitter {
124
124
  self.enableStereo = enabled
125
125
  }
126
126
  }
127
-
127
+
128
+ @objc(setMuteMode:)
129
+ func setMuteMode(mode: NSInteger) {
130
+ audioSessionQueue.async { [self] in
131
+ guard let adm = getAudioDeviceModule() else {
132
+ log("setMuteMode(\(mode)) skipped: no live call ADM")
133
+ return
134
+ }
135
+ let muteMode = RTCAudioEngineMuteMode(rawValue: mode) ?? .voiceProcessing
136
+ _ = adm.setMuteMode(muteMode)
137
+ }
138
+ }
139
+
140
+ @objc(setRecordingAlwaysPreparedMode:)
141
+ func setRecordingAlwaysPreparedMode(enabled: Bool) {
142
+ audioSessionQueue.async { [self] in
143
+ guard let adm = getAudioDeviceModule() else {
144
+ log("setRecordingAlwaysPreparedMode(\(enabled)) skipped: no live call ADM")
145
+ return
146
+ }
147
+ _ = adm.setRecordingAlwaysPreparedMode(enabled)
148
+ }
149
+ }
150
+
128
151
  /// Builds the audio config for the current role/device and sets it as WebRTC's default.
129
152
  private func makeAudioConfiguration(for routing: OutputRouting?) -> RTCAudioSessionConfiguration {
130
153
  let category: AVAudioSession.Category
@@ -152,7 +175,7 @@ class StreamInCallManager: RCTEventEmitter {
152
175
  // only exists under .playAndRecord.
153
176
  category = .playback
154
177
  mode = .spokenAudio
155
- options = []
178
+ options = [.mixWithOthers]
156
179
  } else {
157
180
  // XCode 16 and older don't expose .allowBluetoothHFP
158
181
  // https://forums.swift.org/t/xcode-26-avaudiosession-categoryoptions-allowbluetooth-deprecated/80956
@@ -170,7 +193,7 @@ class StreamInCallManager: RCTEventEmitter {
170
193
  // then we always can't use defaultToSpeaker
171
194
  mustSetDefaultToSpeaker = false
172
195
  }
173
- options = mustSetDefaultToSpeaker ? [bluetoothOption, .defaultToSpeaker] : [bluetoothOption]
196
+ options = mustSetDefaultToSpeaker ? [bluetoothOption, .defaultToSpeaker, .mixWithOthers] : [bluetoothOption, .mixWithOthers]
174
197
  }
175
198
 
176
199
  let rtcConfig = RTCAudioSessionConfiguration.webRTC()
@@ -199,11 +222,11 @@ class StreamInCallManager: RCTEventEmitter {
199
222
  // This path (no CallKit) owns audio-session activation itself, so the
200
223
  // engine must be allowed to start. Restores availability in case a
201
224
  // prior CallKit call had gated it off.
202
- _ = adm.setEngineAvailability(.default)
225
+ _ = adm?.setEngineAvailability(.default)
203
226
 
204
227
  // Stereo is listener-only and applies live. Cleared by stop()'s reset().
205
228
  if callAudioRole == .listener && enableStereo {
206
- adm.setStereoPlayoutPreference(true)
229
+ adm?.setStereoPlayoutPreference(true)
207
230
  }
208
231
 
209
232
  let rtcConfig = makeAudioConfiguration(for: selectedOutput)
@@ -228,7 +251,7 @@ class StreamInCallManager: RCTEventEmitter {
228
251
  #if DEBUG
229
252
  NSLog("%@","[StreamInCallManager][wireEngineSubscription]")
230
253
  #endif
231
- engineSubscription = adm.publisher.sink { [weak self] event in
254
+ engineSubscription = adm?.publisher.sink { [weak self] event in
232
255
  guard let self else { return }
233
256
  self.audioSessionQueue.async {
234
257
  switch event {
@@ -339,7 +362,7 @@ class StreamInCallManager: RCTEventEmitter {
339
362
  // session.setActive(true) here.
340
363
  do {
341
364
  let adm = getAudioDeviceModule()
342
- try adm.setPlayout(true)
365
+ try adm?.setPlayout(true)
343
366
  self.log("adm.setPlayout(true) done")
344
367
  } catch {
345
368
  // String(describing:) surfaces the real error code (see setup()).
@@ -357,7 +380,7 @@ class StreamInCallManager: RCTEventEmitter {
357
380
  return
358
381
  }
359
382
  let adm = getAudioDeviceModule()
360
- adm.reset()
383
+ adm?.reset()
361
384
  clearOutputRouting()
362
385
  // Deactivate directly: the .didDisableAudioEngine sink is async and we cancel it below.
363
386
  applyConfigForEngineDisable()
@@ -603,7 +626,10 @@ class StreamInCallManager: RCTEventEmitter {
603
626
  @objc(getAudioStateLog)
604
627
  func getAudioStateLog() -> String {
605
628
  let session = AVAudioSession.sharedInstance()
606
- let adm = getAudioDeviceModule()
629
+
630
+ guard let adm = getAudioDeviceModule() else {
631
+ return "No audio device module found"
632
+ }
607
633
 
608
634
  // WebRTC wraps AVAudioSession with RTCAudioSession; log its state as well.
609
635
  let rtcSession = RTCAudioSession.sharedInstance()
@@ -798,7 +824,11 @@ class StreamInCallManager: RCTEventEmitter {
798
824
  stereoRefreshWorkItem?.cancel()
799
825
  // Create a new debounced work item
800
826
  let workItem = DispatchWorkItem { [weak self] in
801
- self?.getAudioDeviceModule().refreshStereoPlayoutState()
827
+ guard let adm = self?.getAudioDeviceModule() else {
828
+ self?.log("Audio device module is not ready")
829
+ return
830
+ }
831
+ adm.refreshStereoPlayoutState()
802
832
  self?.log("Executed debounced refreshStereoPlayoutState")
803
833
  }
804
834
  stereoRefreshWorkItem = workItem
@@ -863,12 +893,14 @@ class StreamInCallManager: RCTEventEmitter {
863
893
  }
864
894
 
865
895
  // MARK: - Helper Methods
866
- private func getAudioDeviceModule() -> AudioDeviceModule {
896
+ private func getAudioDeviceModule() -> AudioDeviceModule? {
867
897
  guard let webrtcModule = moduleRegistry?.module(forName: "WebRTCModule") as? WebRTCModule else {
868
898
  fatalError("WebRTCModule is required but not registered with the module registry")
869
899
  }
870
900
 
871
- return webrtcModule.audioDeviceModule
901
+ // Follow the live call's ADM; fall back to the default only when no call factory is
902
+ // active (bare-fork, or an in-call-manager op firing outside the join↔leave window).
903
+ return webrtcModule.currentAudioDeviceModuleOrNil()
872
904
  }
873
905
 
874
906
  private func getCurrentWindow() -> UIWindow? {
@@ -51,6 +51,7 @@ void broadcastNotificationCallback(CFNotificationCenterRef center,
51
51
  bool hasListeners;
52
52
  CFNotificationCenterRef _notificationCenter;
53
53
  AVAudioPlayer *_busyTonePlayer; // Instance variable
54
+ ScreenAudioCapture *_screenAudioCapture; // Broadcast screen-share audio receiver
54
55
  }
55
56
 
56
57
  RCT_EXPORT_MODULE();
@@ -647,19 +648,25 @@ RCT_EXPORT_METHOD(startScreenShareAudioMixing:(RCTPromiseResolveBlock)resolve
647
648
  if (apmId && [apmId isKindOfClass:[RTCDefaultAudioProcessingModule class]]) {
648
649
  RTCDefaultAudioProcessingModule *apm = (RTCDefaultAudioProcessingModule *)apmId;
649
650
  apm.capturePostProcessingDelegate = mixer;
650
- NSLog(@"[SSAMixer] Set capturePostProcessingDelegate on APM");
651
651
  } else {
652
- NSLog(@"[SSAMixer] WARNING: No RTCDefaultAudioProcessingModule available, mixing will not work");
652
+ NSLog(@"[SSAudio][Native] WARNING: No RTCDefaultAudioProcessingModule available, screen-share audio mixing will not work");
653
653
  }
654
654
 
655
655
  [mixer startMixing];
656
656
 
657
- // Wire audio buffer handler on the active capturer → mixer.enqueue
658
657
  InAppScreenCapturer *capturer = options.activeInAppScreenCapturer;
659
658
  if (capturer) {
660
659
  capturer.audioBufferHandler = ^(CMSampleBufferRef sampleBuffer) {
661
660
  [mixer enqueue:sampleBuffer];
662
661
  };
662
+ } else {
663
+ if (!_screenAudioCapture) {
664
+ _screenAudioCapture = [ScreenAudioCapture new];
665
+ }
666
+ _screenAudioCapture.onAudioBuffer = ^(AVAudioPCMBuffer *pcmBuffer) {
667
+ [mixer enqueuePCM:pcmBuffer];
668
+ };
669
+ [_screenAudioCapture start];
663
670
  }
664
671
 
665
672
  resolve(nil);
@@ -671,12 +678,16 @@ RCT_EXPORT_METHOD(stopScreenShareAudioMixing:(RCTPromiseResolveBlock)resolve
671
678
  WebRTCModule *webrtcModule = [self.moduleRegistry moduleForName:"WebRTCModule"];
672
679
  WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance];
673
680
 
674
- // Stop feeding audio to the mixer
675
681
  InAppScreenCapturer *capturer = options.activeInAppScreenCapturer;
676
682
  if (capturer) {
677
683
  capturer.audioBufferHandler = nil;
678
684
  }
679
685
 
686
+ if (_screenAudioCapture) {
687
+ _screenAudioCapture.onAudioBuffer = nil;
688
+ [_screenAudioCapture stop];
689
+ }
690
+
680
691
  // Stop mixing
681
692
  ScreenShareAudioMixer *mixer = webrtcModule.audioDeviceModule.screenShareAudioMixer;
682
693
  [mixer stopMixing];
@@ -686,7 +697,6 @@ RCT_EXPORT_METHOD(stopScreenShareAudioMixing:(RCTPromiseResolveBlock)resolve
686
697
  if (apmId && [apmId isKindOfClass:[RTCDefaultAudioProcessingModule class]]) {
687
698
  RTCDefaultAudioProcessingModule *apm = (RTCDefaultAudioProcessingModule *)apmId;
688
699
  apm.capturePostProcessingDelegate = nil;
689
- NSLog(@"[SSAMixer] Cleared capturePostProcessingDelegate on APM");
690
700
  }
691
701
 
692
702
  resolve(nil);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-react-native-sdk",
3
- "version": "1.42.0",
3
+ "version": "1.44.0",
4
4
  "description": "Stream Video SDK for React Native",
5
5
  "author": "https://getstream.io",
6
6
  "homepage": "https://getstream.io/video/docs/react-native/",
@@ -16,7 +16,7 @@
16
16
  "build:expo-plugin": "rimraf expo-config-plugin/dist && tsc --project expo-config-plugin/tsconfig.json",
17
17
  "build": "yarn copy-version && bob build && yarn build:expo-plugin",
18
18
  "test:expo-plugin": "jest --config=expo-config-plugin/jest.config.js --coverage",
19
- "test": "yarn copy-version && jest --coverage && yarn test:expo-plugin",
19
+ "test": "yarn copy-version && jest --coverage && yarn test:types && yarn test:expo-plugin",
20
20
  "test:types": "yarn copy-version && tsc -p tsconfig.spec.json",
21
21
  "copy-version": "echo \"export const version = '$npm_package_version';\" > ./src/version.ts"
22
22
  },
@@ -52,8 +52,8 @@
52
52
  "!**/.*"
53
53
  ],
54
54
  "dependencies": {
55
- "@stream-io/video-client": "1.56.0",
56
- "@stream-io/video-react-bindings": "1.19.1",
55
+ "@stream-io/video-client": "1.58.0",
56
+ "@stream-io/video-react-bindings": "1.20.0",
57
57
  "intl-pluralrules": "2.0.1",
58
58
  "react-native-url-polyfill": "^3.0.0",
59
59
  "rxjs": "~7.8.2",
@@ -65,7 +65,7 @@
65
65
  "@react-native-firebase/messaging": ">=17.5.0",
66
66
  "@stream-io/noise-cancellation-react-native": ">=0.1.0",
67
67
  "@stream-io/react-native-callingx": ">=0.1.0",
68
- "@stream-io/react-native-webrtc": "^145.1.1",
68
+ "@stream-io/react-native-webrtc": "^145.3.1",
69
69
  "@stream-io/video-filters-react-native": ">=0.1.0",
70
70
  "expo": ">=47.0.0",
71
71
  "expo-notifications": "*",
@@ -106,37 +106,39 @@
106
106
  },
107
107
  "devDependencies": {
108
108
  "@babel/core": "^7.29.7",
109
- "@expo/config-plugins": "56.0.8",
110
- "@expo/config-types": "^56.0.5",
109
+ "@babel/preset-env": "^7.29.7",
110
+ "@babel/preset-typescript": "^7.29.7",
111
+ "@expo/config-plugins": "^57.0.6",
112
+ "@expo/config-types": "^57.0.2",
111
113
  "@expo/plist": "^0.7.0",
112
114
  "@react-native-community/netinfo": "12.0.1",
113
- "@react-native-firebase/app": "^24.0.0",
114
- "@react-native-firebase/messaging": "^24.0.0",
115
- "@react-native/babel-preset": "0.85.3",
116
- "@react-native/metro-config": "0.85.3",
117
- "@stream-io/noise-cancellation-react-native": "^0.9.5",
118
- "@stream-io/react-native-callingx": "^0.8.1",
119
- "@stream-io/react-native-webrtc": "145.1.1",
115
+ "@react-native-firebase/app": "^24.1.1",
116
+ "@react-native-firebase/messaging": "^24.1.1",
117
+ "@react-native/babel-preset": "0.86.2",
118
+ "@react-native/jest-preset": "0.86.2",
119
+ "@react-native/metro-config": "0.86.2",
120
+ "@stream-io/noise-cancellation-react-native": "^0.11.0",
121
+ "@stream-io/react-native-callingx": "^0.10.0",
122
+ "@stream-io/react-native-webrtc": "145.3.1",
120
123
  "@stream-io/typescript-config": "^0.0.0",
121
- "@stream-io/video-filters-react-native": "^0.14.5",
124
+ "@stream-io/video-filters-react-native": "^0.16.0",
122
125
  "@testing-library/jest-native": "^5.4.3",
123
126
  "@testing-library/react-native": "13.3.3",
124
127
  "@tsconfig/node18": "^18.2.6",
125
128
  "@types/jest": "^29.5.14",
126
- "@types/node": "^25.9.3",
127
- "@types/react": "~19.2.15",
129
+ "@types/node": "^25.9.5",
130
+ "@types/react": "~19.2.18",
128
131
  "@types/react-test-renderer": "^19.1.0",
129
- "expo": "~56.0.9",
130
- "expo-module-scripts": "^56.0.3",
131
- "expo-notifications": "~56.0.16",
132
+ "expo": "~57.0.9",
133
+ "expo-notifications": "~57.0.8",
132
134
  "jest": "^29.7.0",
133
135
  "react": "19.2.3",
134
- "react-native": "0.85.3",
135
- "react-native-builder-bob": "^0.41.0",
136
- "react-native-gesture-handler": "~2.31.2",
137
- "react-native-reanimated": "4.3.1",
136
+ "react-native": "0.86.2",
137
+ "react-native-builder-bob": "^0.43.0",
138
+ "react-native-gesture-handler": "~2.32.0",
139
+ "react-native-reanimated": "4.5.3",
138
140
  "react-native-svg": "15.15.5",
139
- "react-native-worklets": "0.8.3",
141
+ "react-native-worklets": "0.10.1",
140
142
  "react-test-renderer": "19.2.3",
141
143
  "rimraf": "^6.1.3",
142
144
  "typescript": "^6.0.3"
@@ -8,7 +8,7 @@ import {
8
8
  } from 'react-native';
9
9
  import { useCall } from '@stream-io/video-react-bindings';
10
10
  import {
11
- type SendReactionRequest,
11
+ type SendVideoReactionRequest,
12
12
  videoLoggerSystem,
13
13
  } from '@stream-io/video-client';
14
14
  import { ComponentTestIds } from '../../../../constants/TestIds';
@@ -82,7 +82,7 @@ export const ReactionsPicker = ({
82
82
  left: reactionsButtonLayoutRectangle?.x,
83
83
  };
84
84
 
85
- const onClose = (reaction?: SendReactionRequest) => {
85
+ const onClose = (reaction?: SendVideoReactionRequest) => {
86
86
  if (reaction) {
87
87
  call?.sendReaction(reaction).catch((e) => {
88
88
  const logger = videoLoggerSystem.getLogger('ReactionsPicker');
@@ -7,9 +7,8 @@ import {
7
7
  } from '@stream-io/video-react-bindings';
8
8
  import { Avatar } from '../../utility/Avatar';
9
9
  import type { StreamVideoParticipant } from '@stream-io/video-client';
10
- import type { MediaStream } from '@stream-io/react-native-webrtc';
11
- import { RTCView } from '@stream-io/react-native-webrtc';
12
10
  import { LobbyControls as DefaultLobbyControls } from '../CallControls/LobbyControls';
11
+ import { LobbyCameraPreview } from './LobbyCameraPreview';
13
12
  import {
14
13
  JoinCallButton as DefaultJoinCallButton,
15
14
  type JoinCallButtonProps,
@@ -65,9 +64,8 @@ export const Lobby = ({
65
64
  const { useCameraState, useCallSettings } = useCallStateHooks();
66
65
  const callSettings = useCallSettings();
67
66
  const isVideoEnabledInCall = callSettings?.video.enabled;
68
- const { isMute: cameraIsMuted, mediaStream } = useCameraState();
67
+ const { optimisticIsMute: cameraIsMuted } = useCameraState();
69
68
  const { t } = useI18n();
70
- const localVideoStream = mediaStream as unknown as MediaStream | undefined;
71
69
 
72
70
  const connectedUserAsParticipant = useMemo(
73
71
  () =>
@@ -98,13 +96,8 @@ export const Lobby = ({
98
96
  ]}
99
97
  >
100
98
  <View style={styles.topView} />
101
- {!cameraIsMuted && localVideoStream ? (
102
- <RTCView
103
- mirror={true}
104
- streamURL={localVideoStream.toURL()}
105
- objectFit="cover"
106
- style={StyleSheet.absoluteFill}
107
- />
99
+ {!cameraIsMuted ? (
100
+ <LobbyCameraPreview objectFit="cover" />
108
101
  ) : (
109
102
  <View style={[styles.avatarContainer, lobby.avatarContainer]}>
110
103
  <Avatar participant={connectedUserAsParticipant} />
@@ -0,0 +1,99 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Platform, StyleSheet, ViewStyle } from 'react-native';
3
+ import {
4
+ permissions,
5
+ RTCCameraPreviewView,
6
+ } from '@stream-io/react-native-webrtc';
7
+ import { useCallStateHooks } from '@stream-io/video-react-bindings';
8
+
9
+ /**
10
+ * Props for the {@link LobbyCameraPreview} component.
11
+ */
12
+ export type LobbyCameraPreviewProps = {
13
+ /**
14
+ * Resembles the CSS style object-fit.
15
+ *
16
+ * @default 'cover'
17
+ */
18
+ objectFit?: 'contain' | 'cover';
19
+ /**
20
+ * Style applied to the preview view.
21
+ */
22
+ style?: ViewStyle;
23
+ };
24
+
25
+ /**
26
+ * The lobby runs before the call is joined. This component drives the native
27
+ * camera capturer directly via {@link RTCCameraPreviewView} which requires
28
+ * no track/peer connection factory to be created.
29
+ *
30
+ * It is driven by the *intended* camera state (optimistic): the lobby camera
31
+ * toggle only updates `optimisticStatus`; the real WebRTC track is acquired and
32
+ * published at join.
33
+ */
34
+ export const LobbyCameraPreview = ({
35
+ style,
36
+ objectFit = 'cover',
37
+ }: LobbyCameraPreviewProps) => {
38
+ const { useCameraState, useCallSettings } = useCallStateHooks();
39
+
40
+ const { optimisticIsMute, direction, selectedDevice } = useCameraState();
41
+ const settings = useCallSettings();
42
+
43
+ const wantsCamera = !optimisticIsMute;
44
+ const hasCameraPermission = useEnsureCameraPermission(wantsCamera);
45
+
46
+ const facing = direction === 'back' ? 'back' : 'front';
47
+
48
+ // Capture at the call's target resolution so the running preview capturer matches
49
+ // what the track publishes and can be adopted at join without reconfiguring.
50
+ // Normalize to landscape, matching CameraManager.selectTargetResolution.
51
+ const targetResolution = settings?.video.target_resolution;
52
+ let captureWidth = targetResolution?.width ?? 1280;
53
+ let captureHeight = targetResolution?.height ?? 720;
54
+ if (captureWidth < captureHeight) {
55
+ [captureWidth, captureHeight] = [captureHeight, captureWidth];
56
+ }
57
+
58
+ return (
59
+ <RTCCameraPreviewView
60
+ style={[StyleSheet.absoluteFill, style as any]}
61
+ isActive={wantsCamera && hasCameraPermission}
62
+ facing={facing}
63
+ deviceId={selectedDevice}
64
+ mirror={Platform.OS === 'ios' ? true : facing === 'front'}
65
+ objectFit={objectFit}
66
+ captureWidth={captureWidth}
67
+ captureHeight={captureHeight}
68
+ />
69
+ );
70
+ };
71
+
72
+ const useEnsureCameraPermission = (enabled: boolean): boolean => {
73
+ const [granted, setGranted] = useState(false);
74
+
75
+ useEffect(() => {
76
+ if (!enabled || granted) return;
77
+
78
+ let cancelled = false;
79
+ (async () => {
80
+ try {
81
+ const status = await permissions.query({ name: 'camera' });
82
+ if (status === 'granted') {
83
+ if (!cancelled) setGranted(true);
84
+ return;
85
+ }
86
+ const result = await permissions.request({ name: 'camera' });
87
+ if (!cancelled) setGranted(!!result);
88
+ } catch {
89
+ if (!cancelled) setGranted(false);
90
+ }
91
+ })();
92
+
93
+ return () => {
94
+ cancelled = true;
95
+ };
96
+ }, [enabled, granted]);
97
+
98
+ return granted;
99
+ };
@@ -1,3 +1,4 @@
1
1
  export * from './Lobby';
2
+ export * from './LobbyCameraPreview';
2
3
  export * from './JoinCallButton';
3
4
  export * from './LobbyFooter';
@@ -109,8 +109,7 @@ const Background = () => {
109
109
  const { useCameraState } = useCallStateHooks();
110
110
  const { isMute, camera } = useCameraState();
111
111
  const localVideoStream = camera.state.mediaStream as unknown as
112
- | MediaStream
113
- | undefined;
112
+ MediaStream | undefined;
114
113
 
115
114
  if (isMute || !localVideoStream) {
116
115
  return (
@@ -25,10 +25,7 @@ import { useFloatingVideoDimensions } from './useFloatingVideoDimensions';
25
25
  import { type StreamVideoParticipant } from '@stream-io/video-client';
26
26
 
27
27
  export type FloatingParticipantViewAlignment =
28
- | 'top-left'
29
- | 'top-right'
30
- | 'bottom-left'
31
- | 'bottom-right';
28
+ 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
32
29
 
33
30
  /**
34
31
  * Props to be passed for the LocalVideoView component.