@stream-io/video-react-native-sdk 1.37.1-beta.0 → 1.38.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 (77) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt +0 -81
  3. package/dist/commonjs/hooks/index.js +0 -11
  4. package/dist/commonjs/hooks/index.js.map +1 -1
  5. package/dist/commonjs/modules/call-manager/CallManager.js +13 -0
  6. package/dist/commonjs/modules/call-manager/CallManager.js.map +1 -1
  7. package/dist/commonjs/providers/StreamCall/AudioInterruptionTracer.js +37 -0
  8. package/dist/commonjs/providers/StreamCall/AudioInterruptionTracer.js.map +1 -0
  9. package/dist/commonjs/providers/StreamCall/index.js +2 -1
  10. package/dist/commonjs/providers/StreamCall/index.js.map +1 -1
  11. package/dist/commonjs/utils/internal/callingx/callingx.js +2 -2
  12. package/dist/commonjs/utils/internal/callingx/callingx.js.map +1 -1
  13. package/dist/commonjs/utils/push/internal/ios.js +5 -0
  14. package/dist/commonjs/utils/push/internal/ios.js.map +1 -1
  15. package/dist/commonjs/version.js +1 -1
  16. package/dist/commonjs/version.js.map +1 -1
  17. package/dist/module/hooks/index.js +0 -1
  18. package/dist/module/hooks/index.js.map +1 -1
  19. package/dist/module/modules/call-manager/CallManager.js +13 -0
  20. package/dist/module/modules/call-manager/CallManager.js.map +1 -1
  21. package/dist/module/providers/StreamCall/AudioInterruptionTracer.js +30 -0
  22. package/dist/module/providers/StreamCall/AudioInterruptionTracer.js.map +1 -0
  23. package/dist/module/providers/StreamCall/index.js +2 -1
  24. package/dist/module/providers/StreamCall/index.js.map +1 -1
  25. package/dist/module/utils/internal/callingx/callingx.js +2 -2
  26. package/dist/module/utils/internal/callingx/callingx.js.map +1 -1
  27. package/dist/module/utils/push/internal/ios.js +5 -0
  28. package/dist/module/utils/push/internal/ios.js.map +1 -1
  29. package/dist/module/version.js +1 -1
  30. package/dist/module/version.js.map +1 -1
  31. package/dist/typescript/hooks/index.d.ts +0 -1
  32. package/dist/typescript/hooks/index.d.ts.map +1 -1
  33. package/dist/typescript/modules/call-manager/CallManager.d.ts +8 -1
  34. package/dist/typescript/modules/call-manager/CallManager.d.ts.map +1 -1
  35. package/dist/typescript/modules/call-manager/types.d.ts +6 -0
  36. package/dist/typescript/modules/call-manager/types.d.ts.map +1 -1
  37. package/dist/typescript/providers/StreamCall/AudioInterruptionTracer.d.ts +5 -0
  38. package/dist/typescript/providers/StreamCall/AudioInterruptionTracer.d.ts.map +1 -0
  39. package/dist/typescript/providers/StreamCall/index.d.ts.map +1 -1
  40. package/dist/typescript/utils/push/internal/ios.d.ts.map +1 -1
  41. package/dist/typescript/version.d.ts +1 -1
  42. package/dist/typescript/version.d.ts.map +1 -1
  43. package/ios/PictureInPicture/StreamBufferTransformer.swift +13 -4
  44. package/ios/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +76 -70
  45. package/ios/PictureInPicture/StreamRTCYUVBuffer.swift +20 -16
  46. package/ios/StreamInCallManager.swift +237 -81
  47. package/ios/StreamVideoReactNative-Bridging-Header.h +0 -2
  48. package/ios/StreamVideoReactNative.m +0 -81
  49. package/package.json +11 -11
  50. package/src/hooks/index.ts +0 -1
  51. package/src/modules/call-manager/CallManager.ts +25 -1
  52. package/src/modules/call-manager/types.ts +7 -0
  53. package/src/providers/StreamCall/AudioInterruptionTracer.tsx +51 -0
  54. package/src/providers/StreamCall/index.tsx +2 -0
  55. package/src/utils/internal/callingx/callingx.ts +2 -2
  56. package/src/utils/push/internal/ios.ts +5 -0
  57. package/src/version.ts +1 -1
  58. package/android/src/main/java/com/streamvideo/reactnative/recorder/AudioPipeline.kt +0 -436
  59. package/android/src/main/java/com/streamvideo/reactnative/recorder/EncoderConstants.kt +0 -17
  60. package/android/src/main/java/com/streamvideo/reactnative/recorder/PipelineHost.kt +0 -36
  61. package/android/src/main/java/com/streamvideo/reactnative/recorder/RecorderPlaybackSamplesSink.kt +0 -60
  62. package/android/src/main/java/com/streamvideo/reactnative/recorder/RecorderVideoSink.kt +0 -31
  63. package/android/src/main/java/com/streamvideo/reactnative/recorder/TracksRecorderManager.kt +0 -329
  64. package/android/src/main/java/com/streamvideo/reactnative/recorder/VideoPipeline.kt +0 -472
  65. package/dist/commonjs/hooks/useLoopbackRecording.js +0 -243
  66. package/dist/commonjs/hooks/useLoopbackRecording.js.map +0 -1
  67. package/dist/module/hooks/useLoopbackRecording.js +0 -238
  68. package/dist/module/hooks/useLoopbackRecording.js.map +0 -1
  69. package/dist/typescript/hooks/useLoopbackRecording.d.ts +0 -85
  70. package/dist/typescript/hooks/useLoopbackRecording.d.ts.map +0 -1
  71. package/ios/TracksRecorder/AudioPipeline.swift +0 -270
  72. package/ios/TracksRecorder/PipelineHost.swift +0 -56
  73. package/ios/TracksRecorder/RecorderAudioRenderTap.swift +0 -154
  74. package/ios/TracksRecorder/RecorderVideoSink.swift +0 -137
  75. package/ios/TracksRecorder/TracksRecorderManager.swift +0 -327
  76. package/ios/TracksRecorder/VideoPipeline.swift +0 -297
  77. package/src/hooks/useLoopbackRecording.ts +0 -438
@@ -2,6 +2,7 @@ import Foundation
2
2
  import React
3
3
  import UIKit
4
4
  import AVFoundation
5
+ import Combine
5
6
  import stream_react_native_webrtc
6
7
  import AVKit
7
8
  import MediaPlayer
@@ -29,6 +30,10 @@ private enum Constants {
29
30
  static let stereoRefreshDebounceSeconds: TimeInterval = 0.5
30
31
  }
31
32
 
33
+ private enum StreamInCallManagerEvents {
34
+ static let audioInterruption = "StreamInCallManagerAudioInterruption"
35
+ }
36
+
32
37
  @objc(StreamInCallManager)
33
38
  class StreamInCallManager: RCTEventEmitter {
34
39
 
@@ -47,7 +52,11 @@ class StreamInCallManager: RCTEventEmitter {
47
52
  }
48
53
 
49
54
  private var hasRegisteredRouteObserver = false
55
+ private var hasRegisteredInterruptionObserver = false
50
56
  private var stereoRefreshWorkItem: DispatchWorkItem?
57
+ /// Combine subscription to the AudioDeviceModule's engine-lifecycle publisher.
58
+ /// Wired in `setup()`; torn down in `stop()`.
59
+ private var engineSubscription: AnyCancellable?
51
60
 
52
61
  override func invalidate() {
53
62
  stop()
@@ -91,69 +100,149 @@ class StreamInCallManager: RCTEventEmitter {
91
100
  }
92
101
  }
93
102
 
103
+ /// Builds the audio config for the current role/device and sets it as WebRTC's default.
104
+ private func makeAudioConfiguration() -> RTCAudioSessionConfiguration {
105
+ let category: AVAudioSession.Category
106
+ let mode: AVAudioSession.Mode
107
+ let options: AVAudioSession.CategoryOptions
108
+
109
+ if callAudioRole == .listener {
110
+ // High quality audio playback, microphone disabled.
111
+ // .spokenAudio + .mixWithOthers: listener flow is passive spoken-audio
112
+ // playback, so let other apps' audio coexist (music keeps playing under it)
113
+ // and let the system handle ducking semantics for spoken content.
114
+ category = .playback
115
+ mode = .spokenAudio
116
+ options = [.mixWithOthers]
117
+ // TODO: for stereo we should disallow BluetoothHFP and allow only allowBluetoothA2DP
118
+ // note: this is the behaviour of iOS native SDK, but fails here with (OSStatus error -50.)
119
+ // options = self.enableStereo ? [.allowBluetoothA2DP] : []
120
+ } else {
121
+ // XCode 16 and older don't expose .allowBluetoothHFP
122
+ // https://forums.swift.org/t/xcode-26-avaudiosession-categoryoptions-allowbluetooth-deprecated/80956
123
+ #if compiler(>=6.2) // For Xcode 26.0+
124
+ let bluetoothOption: AVAudioSession.CategoryOptions = .allowBluetoothHFP
125
+ #else
126
+ let bluetoothOption: AVAudioSession.CategoryOptions = .allowBluetooth
127
+ #endif
128
+ category = .playAndRecord
129
+ mode = .voiceChat
130
+ options = defaultAudioDevice == .speaker ? [bluetoothOption, .defaultToSpeaker] : [bluetoothOption]
131
+ }
132
+
133
+ let rtcConfig = RTCAudioSessionConfiguration.webRTC()
134
+ rtcConfig.category = category.rawValue
135
+ rtcConfig.mode = mode.rawValue
136
+ rtcConfig.categoryOptions = options
137
+ // Keep WebRTC's internal state consistent during interruptions/route changes.
138
+ RTCAudioSessionConfiguration.setWebRTC(rtcConfig)
139
+ return rtcConfig
140
+ }
141
+
94
142
  @objc
95
143
  func setup() {
96
144
  audioSessionQueue.async { [self] in
97
- let intendedCategory: AVAudioSession.Category!
98
- let intendedMode: AVAudioSession.Mode!
99
- let intendedOptions: AVAudioSession.CategoryOptions!
100
-
101
145
  let adm = getAudioDeviceModule()
102
- let wasRecording = adm.isRecording
103
- let wasPlaying = adm.isPlaying
104
- adm.reset()
105
146
 
106
- if (callAudioRole == .listener) {
107
- // enables high quality audio playback but disables microphone
108
- intendedCategory = .playback
109
- intendedMode = .default
110
- intendedOptions = []
111
- // TODO: for stereo we should disallow BluetoothHFP and allow only allowBluetoothA2DP
112
- // note: this is the behaviour of iOS native SDK, but fails here with (OSStatus error -50.)
113
- // intendedOptions = self.enableStereo ? [.allowBluetoothA2DP] : []
114
- if (self.enableStereo) {
115
- adm.setStereoPlayoutPreference(true)
116
- }
117
- } else {
118
- intendedCategory = .playAndRecord
119
- intendedMode = .voiceChat
120
-
121
- // XCode 16 and older don't expose .allowBluetoothHFP
122
- // https://forums.swift.org/t/xcode-26-avaudiosession-categoryoptions-allowbluetooth-deprecated/80956
123
- #if compiler(>=6.2) // For Xcode 26.0+
124
- let bluetoothOption: AVAudioSession.CategoryOptions = .allowBluetoothHFP
125
- #else
126
- let bluetoothOption: AVAudioSession.CategoryOptions = .allowBluetooth
127
- #endif
128
- intendedOptions = defaultAudioDevice == .speaker ? [bluetoothOption, .defaultToSpeaker] : [bluetoothOption]
147
+ // Stereo is listener-only and applies live. Cleared by stop()'s reset().
148
+ if callAudioRole == .listener && enableStereo {
149
+ adm.setStereoPlayoutPreference(true)
129
150
  }
130
- log("Setup with category: \(intendedCategory.rawValue), mode: \(intendedMode.rawValue), options: \(String(describing: intendedOptions))")
131
- let rtcConfig = RTCAudioSessionConfiguration.webRTC()
132
- rtcConfig.category = intendedCategory.rawValue
133
- rtcConfig.mode = intendedMode.rawValue
134
- rtcConfig.categoryOptions = intendedOptions
135
- // This ensures WebRTC's internal state stays consistent during interruptions/route changes
136
- RTCAudioSessionConfiguration.setWebRTC(rtcConfig)
137
-
151
+
152
+ let rtcConfig = makeAudioConfiguration()
153
+ log("Setup with category: \(rtcConfig.category), mode: \(rtcConfig.mode), options: \(rtcConfig.categoryOptions)")
154
+
155
+ // Set category only — the sink owns setActive().
138
156
  let session = RTCAudioSession.sharedInstance()
139
157
  session.lockForConfiguration()
140
- defer {
141
- session.unlockForConfiguration()
142
- }
158
+ defer { session.unlockForConfiguration() }
143
159
  do {
144
160
  try session.setConfiguration(rtcConfig)
145
- if (wasRecording) {
146
- try adm.setRecording(wasRecording)
147
- }
148
- if (wasPlaying) {
149
- try adm.setPlayout(wasPlaying)
150
- }
151
161
  } catch {
152
- log("Error setting audio session: \(error.localizedDescription)")
162
+ // String(describing:) shows the real error; localizedDescription prints a useless "error 0".
163
+ log("Error setting audio session: \(String(describing: error))")
164
+ }
165
+
166
+ // Subscribe to the AudioDeviceModule's engine lifecycle (idempotent).
167
+ // The sink does the authoritative reapply on every engine rebuild
168
+ // (interruption recovery, mode swap) and owns `setActive(true/false)`
169
+ // since there's no CallKit on this path.
170
+ // Skipped when callingx owns the session (CallKit-managed call active).
171
+ if engineSubscription == nil {
172
+ #if DEBUG
173
+ NSLog("%@","[StreamInCallManager][wireEngineSubscription]")
174
+ #endif
175
+ engineSubscription = adm.publisher.sink { [weak self] event in
176
+ guard let self else { return }
177
+ self.audioSessionQueue.async {
178
+ switch event {
179
+ case .willEnableAudioEngine:
180
+ self.applyConfigForEngineEnable()
181
+ case .didDisableAudioEngine:
182
+ self.applyConfigForEngineDisable()
183
+ default:
184
+ // .willStartAudioEngine / .didStopAudioEngine intentionally ignored:
185
+ // WebRTC's engine stop/restart around interruptions — it re-activates the
186
+ // session itself via AVAudioEngine.start(). We only (re)apply config +
187
+ // setActive on enable/disable.
188
+ break
189
+ }
190
+ }
191
+ }
153
192
  }
154
193
  }
155
194
  }
156
195
 
196
+ // MARK: - Engine Lifecycle Handlers (non-CallKit path)
197
+
198
+ /// Reapplies the same `AVAudioSessionConfiguration` that `setup()` writes
199
+ /// on every engine rebuild, then activates the session.
200
+ /// No-ops when callingx owns the session.
201
+ private func applyConfigForEngineEnable() {
202
+ if Self.callingxOwnsSession() {
203
+ log("engineWillEnable: callingx owns the session, skipping")
204
+ return
205
+ }
206
+
207
+ let rtcConfig = makeAudioConfiguration()
208
+ let session = RTCAudioSession.sharedInstance()
209
+ session.lockForConfiguration()
210
+ defer { session.unlockForConfiguration() }
211
+ do {
212
+ try session.setConfiguration(rtcConfig)
213
+ try session.setActive(true)
214
+ log("engineWillEnable: applied category=\(rtcConfig.category) mode=\(rtcConfig.mode) activated=true")
215
+ } catch {
216
+ log("engineWillEnable error: \(String(describing: error))")
217
+ }
218
+ }
219
+
220
+ private func applyConfigForEngineDisable() {
221
+ if Self.callingxOwnsSession() {
222
+ return
223
+ }
224
+
225
+ let session = RTCAudioSession.sharedInstance()
226
+ session.lockForConfiguration()
227
+ defer { session.unlockForConfiguration() }
228
+ do {
229
+ try session.setActive(false)
230
+ log("engineDidDisable: deactivated session")
231
+ } catch {
232
+ log("engineDidDisable error: \(String(describing: error))")
233
+ }
234
+ }
235
+
236
+ /// Runtime KVC lookup of `CallingxSessionOwnership.callingxOwnsSession`.
237
+ /// `@stream-io/react-native-callingx` is an optional peer dep, so a direct
238
+ /// Swift `import` is not safe
239
+ private static func callingxOwnsSession() -> Bool {
240
+ guard let cls = NSClassFromString("Callingx.CallingxSessionOwnership") else {
241
+ return false
242
+ }
243
+ return ((cls as AnyObject).value(forKey: "callingxOwnsSession") as? Bool) ?? false
244
+ }
245
+
157
246
  @objc
158
247
  func start() {
159
248
  setup()
@@ -166,25 +255,23 @@ class StreamInCallManager: RCTEventEmitter {
166
255
  UIApplication.shared.isIdleTimerDisabled = true
167
256
  // Register for audio route changes to turn off screen when earpiece is connected
168
257
  self.registerAudioRouteObserver()
258
+ self.registerInterruptionObserver()
169
259
  self.updateProximityMonitoring()
170
260
  self.log("Wake lock enabled (idle timer disabled)")
171
261
  self.log("defaultAudioDevice: \(self.defaultAudioDevice)")
172
262
  }
173
- let session = RTCAudioSession.sharedInstance()
174
- session.lockForConfiguration()
175
- defer {
176
- session.unlockForConfiguration()
177
- }
263
+ // setPlayout(true) triggers .willEnableAudioEngine, whose sink
264
+ // applies the preset and calls setActive(true). No explicit
265
+ // session.setActive(true) here.
178
266
  do {
179
- try session.setActive(true)
180
- self.log("audio session activated")
181
267
  let adm = getAudioDeviceModule()
182
268
  try adm.setPlayout(true)
183
269
  self.log("adm.setPlayout(true) done")
184
270
  } catch {
185
- log("Error activating audio session: \(error.localizedDescription)")
271
+ // String(describing:) surfaces the real error code (see setup()).
272
+ log("Error starting playout: \(String(describing: error))")
186
273
  }
187
-
274
+
188
275
  audioManagerActivated = true
189
276
  }
190
277
  }
@@ -195,18 +282,13 @@ class StreamInCallManager: RCTEventEmitter {
195
282
  if !audioManagerActivated {
196
283
  return
197
284
  }
198
- let session = RTCAudioSession.sharedInstance()
199
- session.lockForConfiguration()
200
- defer {
201
- session.unlockForConfiguration()
202
- }
203
- do {
204
- try session.setActive(false)
205
- let adm = getAudioDeviceModule()
206
- adm.reset()
207
- } catch {
208
- log("Error deactivating audio session: \(error.localizedDescription)")
209
- }
285
+ let adm = getAudioDeviceModule()
286
+ adm.reset()
287
+ // Deactivate directly: the .didDisableAudioEngine sink is async and we cancel it below.
288
+ applyConfigForEngineDisable()
289
+ // Tear down the engine-observer subscription so a re-setup wires a fresh one.
290
+ engineSubscription?.cancel()
291
+ engineSubscription = nil
210
292
  // Cancel any pending debounced stereo refresh
211
293
  stereoRefreshWorkItem?.cancel()
212
294
  stereoRefreshWorkItem = nil
@@ -220,6 +302,7 @@ class StreamInCallManager: RCTEventEmitter {
220
302
  // Disable proximity monitoring to disable earpiece detection
221
303
  self.setProximityMonitoringEnabled(false)
222
304
  self.unregisterAudioRouteObserver()
305
+ self.unregisterInterruptionObserver()
223
306
  // Disable wake lock to allow the screen to dim/lock again
224
307
  UIApplication.shared.isIdleTimerDisabled = false
225
308
  self.log("Wake lock disabled (idle timer enabled)")
@@ -391,6 +474,66 @@ class StreamInCallManager: RCTEventEmitter {
391
474
  log("Unregistered AVAudioSession.routeChangeNotification observer")
392
475
  }
393
476
 
477
+ // MARK: - Interruption Handling
478
+
479
+ /// Observes `AVAudioSession.interruptionNotification` to trace and log *why* an interruption fired
480
+ /// (mic-mute / route-disconnect / PSTN-Siri). Recovery is NOT done here: WebRTC's
481
+ /// AudioEngineDevice owns it — it stops the engine on interruption-begin and restarts it on
482
+ /// interruption-end (re-activating the session itself via AVAudioEngine.start)
483
+ private func registerInterruptionObserver() {
484
+ if hasRegisteredInterruptionObserver { return }
485
+ NotificationCenter.default.addObserver(
486
+ self,
487
+ selector: #selector(handleAudioInterruption(_:)),
488
+ name: AVAudioSession.interruptionNotification,
489
+ object: nil
490
+ )
491
+ hasRegisteredInterruptionObserver = true
492
+ }
493
+
494
+ private func unregisterInterruptionObserver() {
495
+ if !hasRegisteredInterruptionObserver { return }
496
+ NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
497
+ hasRegisteredInterruptionObserver = false
498
+ }
499
+
500
+ @objc
501
+ private func handleAudioInterruption(_ notification: Notification) {
502
+ guard let info = notification.userInfo,
503
+ let typeRaw = info[AVAudioSessionInterruptionTypeKey] as? UInt,
504
+ let type = AVAudioSession.InterruptionType(rawValue: typeRaw) else {
505
+ return
506
+ }
507
+
508
+ let reason = interruptionReason(info)
509
+ var payload: [String: Any] = ["source": "callmanager"]
510
+ if let reason {
511
+ payload["reason"] = reason
512
+ }
513
+
514
+ switch type {
515
+ case .began:
516
+ payload["phase"] = "began"
517
+ sendEvent(withName: StreamInCallManagerEvents.audioInterruption, body: payload)
518
+ #if DEBUG
519
+ log("Audio interruption began (reason=\(reason ?? "n/a")). Recovery owned by WebRTC AudioEngineDevice.")
520
+ #endif
521
+ case .ended:
522
+ var shouldResume = false
523
+ if let optsRaw = info[AVAudioSessionInterruptionOptionKey] as? UInt {
524
+ shouldResume = AVAudioSession.InterruptionOptions(rawValue: optsRaw).contains(.shouldResume)
525
+ }
526
+ payload["phase"] = "ended"
527
+ payload["shouldResume"] = shouldResume
528
+ sendEvent(withName: StreamInCallManagerEvents.audioInterruption, body: payload)
529
+ #if DEBUG
530
+ log("Audio interruption ended (shouldResume=\(shouldResume)). WebRTC restarts the engine.")
531
+ #endif
532
+ @unknown default:
533
+ break
534
+ }
535
+ }
536
+
394
537
  @objc
395
538
  private func handleAudioRouteChange(_ notification: Notification) {
396
539
  guard let userInfo = notification.userInfo,
@@ -454,18 +597,7 @@ class StreamInCallManager: RCTEventEmitter {
454
597
  // MARK: - RCTEventEmitter
455
598
 
456
599
  override func supportedEvents() -> [String]! {
457
- // TODO: list events that can be sent to JS
458
- return []
459
- }
460
-
461
- @objc
462
- override func addListener(_ eventName: String!) {
463
- super.addListener(eventName)
464
- }
465
-
466
- @objc
467
- override func removeListeners(_ count: Double) {
468
- super.removeListeners(count)
600
+ return [StreamInCallManagerEvents.audioInterruption]
469
601
  }
470
602
 
471
603
  // MARK: - Helper Methods
@@ -511,6 +643,30 @@ class StreamInCallManager: RCTEventEmitter {
511
643
  }
512
644
  }
513
645
 
646
+ /// Best-effort name for the iOS 14.5+ `AVAudioSessionInterruptionReasonKey`.
647
+ /// Names the iOS 17 cases we care about and falls back to the raw value otherwise.
648
+ private func interruptionReason(_ info: [AnyHashable: Any]) -> String? {
649
+ guard #available(iOS 14.5, *),
650
+ let reasonRaw = info[AVAudioSessionInterruptionReasonKey] as? UInt,
651
+ let reason = AVAudioSession.InterruptionReason(rawValue: reasonRaw) else {
652
+ return nil
653
+ }
654
+ if #available(iOS 17.0, *) {
655
+ switch reason {
656
+ case .builtInMicMuted:
657
+ return "builtInMicMuted"
658
+ case .routeDisconnected:
659
+ return "routeDisconnected"
660
+ default:
661
+ break
662
+ }
663
+ }
664
+ if reason == .default {
665
+ return "default"
666
+ }
667
+ return "raw(\(reason.rawValue))"
668
+ }
669
+
514
670
  // MARK: - Logging Helper
515
671
  private func log(_ message: String) {
516
672
  NSLog("InCallManager: %@", message)
@@ -11,8 +11,6 @@
11
11
  #import <WebRTC/RTCVideoTrack.h>
12
12
  #import <WebRTC/RTCVideoRenderer.h>
13
13
  #import <WebRTC/RTCVideoFrameBuffer.h>
14
- #import <WebRTC/RTCAudioTrack.h>
15
- #import <WebRTC/RTCAudioRenderer.h>
16
14
  #import "WebRTCModule.h"
17
15
  #import "WebRTCModuleOptions.h"
18
16
 
@@ -20,13 +20,6 @@
20
20
  #import <stream_react_native_webrtc/stream_react_native_webrtc-Swift.h>
21
21
  #endif
22
22
 
23
- // Import Swift-generated header for TracksRecorderManager and friends.
24
- #if __has_include("stream_video_react_native-Swift.h")
25
- #import "stream_video_react_native-Swift.h"
26
- #elif __has_include(<stream_video_react_native/stream_video_react_native-Swift.h>)
27
- #import <stream_video_react_native/stream_video_react_native-Swift.h>
28
- #endif
29
-
30
23
  // Do not change these consts, it is what is used react-native-webrtc
31
24
  NSNotificationName const kBroadcastStartedNotification = @"iOS_BroadcastStarted";
32
25
  NSNotificationName const kBroadcastStoppedNotification = @"iOS_BroadcastStopped";
@@ -692,78 +685,4 @@ RCT_EXPORT_METHOD(stopScreenShareAudioMixing:(RCTPromiseResolveBlock)resolve
692
685
  resolve(nil);
693
686
  }
694
687
 
695
- #pragma mark - Track Recording
696
-
697
- RCT_EXPORT_METHOD(startTrackRecording:(NSDictionary *)options
698
- resolver:(RCTPromiseResolveBlock)resolve
699
- rejecter:(RCTPromiseRejectBlock)reject)
700
- {
701
- WebRTCModule *webrtcModule = [self.bridge moduleForClass:[WebRTCModule class]];
702
- if (!webrtcModule) {
703
- reject(@"recording_error", @"WebRTCModule not available", nil);
704
- return;
705
- }
706
-
707
- NSString *videoTrackId = options[@"videoTrackId"];
708
- if (![videoTrackId isKindOfClass:[NSString class]]) videoTrackId = nil;
709
-
710
- NSNumber *maxDuration = options[@"maxDurationMs"];
711
- NSInteger maxDurationMs = ([maxDuration isKindOfClass:[NSNumber class]])
712
- ? [maxDuration integerValue] : 5000;
713
-
714
- NSNumber *targetW = options[@"targetWidth"];
715
- NSInteger targetWidth = ([targetW isKindOfClass:[NSNumber class]])
716
- ? [targetW integerValue] : 0;
717
-
718
- NSNumber *targetH = options[@"targetHeight"];
719
- NSInteger targetHeight = ([targetH isKindOfClass:[NSNumber class]])
720
- ? [targetH integerValue] : 0;
721
-
722
- [[TracksRecorderManager shared]
723
- startRecordingWithVideoTrackId:videoTrackId
724
- maxDurationMs:maxDurationMs
725
- targetWidth:targetWidth
726
- targetHeight:targetHeight
727
- webRTCModule:webrtcModule
728
- completion:^(NSURL * _Nullable fileURL, NSError * _Nullable err) {
729
- if (err) {
730
- reject(@"recording_error", err.localizedDescription, err);
731
- } else {
732
- resolve(fileURL ? fileURL.absoluteString : [NSNull null]);
733
- }
734
- }];
735
- }
736
-
737
- RCT_EXPORT_METHOD(stopTrackRecording:(RCTPromiseResolveBlock)resolve
738
- rejecter:(RCTPromiseRejectBlock)reject)
739
- {
740
- [[TracksRecorderManager shared] stopRecordingWithCompletion:^{
741
- resolve(nil);
742
- }];
743
- }
744
-
745
- RCT_EXPORT_METHOD(clearStreamRecordings:(RCTPromiseResolveBlock)resolve
746
- rejecter:(RCTPromiseRejectBlock)reject)
747
- {
748
- [[TracksRecorderManager shared] clearRecordingsDirectoryWithCompletion:^(NSError * _Nullable err) {
749
- if (err) {
750
- reject(@"clear_error", err.localizedDescription, err);
751
- } else {
752
- resolve(nil);
753
- }
754
- }];
755
- }
756
-
757
- RCT_EXPORT_METHOD(getStreamRecordings:(RCTPromiseResolveBlock)resolve
758
- rejecter:(RCTPromiseRejectBlock)reject)
759
- {
760
- NSArray<NSURL *> *urls = [[TracksRecorderManager shared] listRecordings];
761
- NSMutableArray<NSString *> *result = [NSMutableArray arrayWithCapacity:urls.count];
762
- for (NSURL *url in urls) {
763
- NSString *abs = url.absoluteString;
764
- if (abs) [result addObject:abs];
765
- }
766
- resolve(result);
767
- }
768
-
769
688
  @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-react-native-sdk",
3
- "version": "1.37.1-beta.0",
3
+ "version": "1.38.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/",
@@ -50,8 +50,8 @@
50
50
  "!**/.*"
51
51
  ],
52
52
  "dependencies": {
53
- "@stream-io/video-client": "1.52.1-beta.0",
54
- "@stream-io/video-react-bindings": "1.16.3-beta.0",
53
+ "@stream-io/video-client": "1.53.0",
54
+ "@stream-io/video-react-bindings": "1.16.3",
55
55
  "intl-pluralrules": "2.0.1",
56
56
  "react-native-url-polyfill": "^3.0.0",
57
57
  "rxjs": "~7.8.2",
@@ -63,7 +63,7 @@
63
63
  "@react-native-firebase/messaging": ">=17.5.0",
64
64
  "@stream-io/noise-cancellation-react-native": ">=0.1.0",
65
65
  "@stream-io/react-native-callingx": ">=0.1.0",
66
- "@stream-io/react-native-webrtc": ">=137.2.2",
66
+ "@stream-io/react-native-webrtc": "^145.0.0",
67
67
  "@stream-io/video-filters-react-native": ">=0.1.0",
68
68
  "expo": ">=47.0.0",
69
69
  "expo-build-properties": "*",
@@ -116,20 +116,20 @@
116
116
  "@react-native-firebase/messaging": "^24.0.0",
117
117
  "@react-native/babel-preset": "0.85.3",
118
118
  "@react-native/metro-config": "0.85.3",
119
- "@stream-io/noise-cancellation-react-native": "0.7.0",
120
- "@stream-io/react-native-callingx": "0.4.0",
121
- "@stream-io/react-native-webrtc": "137.2.2-alpha.0",
122
- "@stream-io/video-filters-react-native": "0.12.4",
119
+ "@stream-io/noise-cancellation-react-native": "^0.8.0",
120
+ "@stream-io/react-native-callingx": "^0.5.0",
121
+ "@stream-io/react-native-webrtc": "145.0.0",
122
+ "@stream-io/video-filters-react-native": "^0.13.0",
123
123
  "@testing-library/jest-native": "^5.4.3",
124
124
  "@testing-library/react-native": "13.3.3",
125
125
  "@tsconfig/node18": "^18.2.6",
126
126
  "@types/jest": "^29.5.14",
127
127
  "@types/react": "~19.2.15",
128
128
  "@types/react-test-renderer": "^19.1.0",
129
- "expo": "~56.0.8",
130
- "expo-build-properties": "~56.0.16",
129
+ "expo": "~56.0.9",
130
+ "expo-build-properties": "~56.0.17",
131
131
  "expo-module-scripts": "^56.0.3",
132
- "expo-notifications": "~56.0.15",
132
+ "expo-notifications": "~56.0.16",
133
133
  "jest": "^29.7.0",
134
134
  "react": "19.2.3",
135
135
  "react-native": "0.85.3",
@@ -7,7 +7,6 @@ export * from './useIsInPiPMode';
7
7
  export * from './useAutoEnterPiPEffect';
8
8
  export * from './useScreenShareButton';
9
9
  export * from './useScreenShareAudioMixing';
10
- export * from './useLoopbackRecording';
11
10
  export * from './useTrackDimensions';
12
11
  export * from './useScreenshot';
13
12
  export * from './useModeration';
@@ -1,10 +1,15 @@
1
1
  import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
2
- import { AudioDeviceStatus, StreamInCallManagerConfig } from './types';
2
+ import type {
3
+ AudioDeviceStatus,
4
+ IOSAudioInterruptionEvent,
5
+ StreamInCallManagerConfig,
6
+ } from './types';
3
7
  import { getCallingxLibIfAvailable } from '../../utils/push/libs/callingx';
4
8
  import { videoLoggerSystem } from '@stream-io/video-client';
5
9
 
6
10
  const NativeManager = NativeModules.StreamInCallManager;
7
11
  const CallingxModule = getCallingxLibIfAvailable();
12
+ const AUDIO_INTERRUPTION_EVENT = 'StreamInCallManagerAudioInterruption';
8
13
 
9
14
  const invariant = (condition: boolean, message: string) => {
10
15
  if (!condition) throw new Error(message);
@@ -46,6 +51,8 @@ class AndroidCallManager {
46
51
  }
47
52
 
48
53
  class IOSCallManager {
54
+ private eventEmitter?: NativeEventEmitter;
55
+
49
56
  /**
50
57
  * Will trigger the iOS device selector.
51
58
  */
@@ -53,6 +60,23 @@ class IOSCallManager {
53
60
  invariant(Platform.OS === 'ios', 'Supported only on iOS');
54
61
  NativeManager.showAudioRoutePicker();
55
62
  };
63
+
64
+ /**
65
+ * Register a listener for iOS audio interruptions.
66
+ *
67
+ * @param onInterruption callback to be called when iOS reports an audio interruption.
68
+ */
69
+ addAudioInterruptionListener = (
70
+ onInterruption: (event: IOSAudioInterruptionEvent) => void,
71
+ ): (() => void) => {
72
+ invariant(Platform.OS === 'ios', 'Supported only on iOS');
73
+ this.eventEmitter ??= new NativeEventEmitter(NativeManager);
74
+ const s = this.eventEmitter.addListener(
75
+ AUDIO_INTERRUPTION_EVENT,
76
+ onInterruption,
77
+ );
78
+ return () => s.remove();
79
+ };
56
80
  }
57
81
 
58
82
  class SpeakerManager {
@@ -15,6 +15,13 @@ export type AudioDeviceStatus = {
15
15
  export type AudioRole = 'communicator' | 'listener';
16
16
  export type DeviceEndpointType = 'speaker' | 'earpiece';
17
17
 
18
+ export type IOSAudioInterruptionEvent = {
19
+ source: 'callmanager' | 'callingx';
20
+ phase: 'began' | 'ended';
21
+ reason?: 'default' | 'builtInMicMuted' | 'routeDisconnected' | (string & {});
22
+ shouldResume?: boolean;
23
+ };
24
+
18
25
  export type StreamInCallManagerConfig =
19
26
  | {
20
27
  audioRole: 'communicator';