@stream-io/video-react-native-sdk 1.43.0 → 1.45.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 (150) hide show
  1. package/CHANGELOG.md +44 -1
  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/Participant/ParticipantView/VideoRenderer/TrackSubscriber.js +4 -15
  10. package/dist/commonjs/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.js.map +1 -1
  11. package/dist/commonjs/components/Participant/ParticipantView/VideoRenderer/index.js +10 -4
  12. package/dist/commonjs/components/Participant/ParticipantView/VideoRenderer/index.js.map +1 -1
  13. package/dist/commonjs/hooks/push/useCallingExpWithCallingStateEffect.js +9 -5
  14. package/dist/commonjs/hooks/push/useCallingExpWithCallingStateEffect.js.map +1 -1
  15. package/dist/commonjs/hooks/useLoopbackRecording.js +10 -127
  16. package/dist/commonjs/hooks/useLoopbackRecording.js.map +1 -1
  17. package/dist/commonjs/modules/call-manager/CallManager.js +37 -54
  18. package/dist/commonjs/modules/call-manager/CallManager.js.map +1 -1
  19. package/dist/commonjs/utils/StreamVideoRN/index.js +2 -0
  20. package/dist/commonjs/utils/StreamVideoRN/index.js.map +1 -1
  21. package/dist/commonjs/utils/internal/callingx/callingx.js +28 -39
  22. package/dist/commonjs/utils/internal/callingx/callingx.js.map +1 -1
  23. package/dist/commonjs/utils/internal/registerMediaEngine.js +41 -0
  24. package/dist/commonjs/utils/internal/registerMediaEngine.js.map +1 -0
  25. package/dist/commonjs/utils/internal/registerSDKGlobals.js +93 -35
  26. package/dist/commonjs/utils/internal/registerSDKGlobals.js.map +1 -1
  27. package/dist/commonjs/utils/push/android.js +13 -175
  28. package/dist/commonjs/utils/push/android.js.map +1 -1
  29. package/dist/commonjs/utils/push/internal/android.js +186 -0
  30. package/dist/commonjs/utils/push/internal/android.js.map +1 -0
  31. package/dist/commonjs/utils/push/libs/firebaseMessaging/index.js +8 -9
  32. package/dist/commonjs/utils/push/libs/firebaseMessaging/index.js.map +1 -1
  33. package/dist/commonjs/utils/push/libs/firebaseMessaging/lib.js +1 -1
  34. package/dist/commonjs/utils/push/libs/firebaseMessaging/lib.js.map +1 -1
  35. package/dist/commonjs/utils/push/setupAndroidPushEvents.js +25 -0
  36. package/dist/commonjs/utils/push/setupAndroidPushEvents.js.map +1 -0
  37. package/dist/commonjs/utils/push/setupCallingExpEvents.js +6 -2
  38. package/dist/commonjs/utils/push/setupCallingExpEvents.js.map +1 -1
  39. package/dist/commonjs/utils/push/setupIosVoipPushEvents.js +6 -7
  40. package/dist/commonjs/utils/push/setupIosVoipPushEvents.js.map +1 -1
  41. package/dist/commonjs/utils/push/utils.js.map +1 -1
  42. package/dist/commonjs/version.js +1 -1
  43. package/dist/module/components/Call/CallControls/internal/ReactionsPicker.js.map +1 -1
  44. package/dist/module/components/Call/Lobby/Lobby.js +4 -9
  45. package/dist/module/components/Call/Lobby/Lobby.js.map +1 -1
  46. package/dist/module/components/Call/Lobby/LobbyCameraPreview.js +87 -0
  47. package/dist/module/components/Call/Lobby/LobbyCameraPreview.js.map +1 -0
  48. package/dist/module/components/Call/Lobby/index.js +1 -0
  49. package/dist/module/components/Call/Lobby/index.js.map +1 -1
  50. package/dist/module/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.js +6 -17
  51. package/dist/module/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.js.map +1 -1
  52. package/dist/module/components/Participant/ParticipantView/VideoRenderer/index.js +11 -5
  53. package/dist/module/components/Participant/ParticipantView/VideoRenderer/index.js.map +1 -1
  54. package/dist/module/hooks/push/useCallingExpWithCallingStateEffect.js +9 -5
  55. package/dist/module/hooks/push/useCallingExpWithCallingStateEffect.js.map +1 -1
  56. package/dist/module/hooks/useLoopbackRecording.js +10 -127
  57. package/dist/module/hooks/useLoopbackRecording.js.map +1 -1
  58. package/dist/module/modules/call-manager/CallManager.js +36 -53
  59. package/dist/module/modules/call-manager/CallManager.js.map +1 -1
  60. package/dist/module/utils/StreamVideoRN/index.js +2 -0
  61. package/dist/module/utils/StreamVideoRN/index.js.map +1 -1
  62. package/dist/module/utils/internal/callingx/callingx.js +27 -40
  63. package/dist/module/utils/internal/callingx/callingx.js.map +1 -1
  64. package/dist/module/utils/internal/registerMediaEngine.js +37 -0
  65. package/dist/module/utils/internal/registerMediaEngine.js.map +1 -0
  66. package/dist/module/utils/internal/registerSDKGlobals.js +95 -37
  67. package/dist/module/utils/internal/registerSDKGlobals.js.map +1 -1
  68. package/dist/module/utils/push/android.js +16 -178
  69. package/dist/module/utils/push/android.js.map +1 -1
  70. package/dist/module/utils/push/internal/android.js +182 -0
  71. package/dist/module/utils/push/internal/android.js.map +1 -0
  72. package/dist/module/utils/push/libs/firebaseMessaging/index.js +8 -8
  73. package/dist/module/utils/push/libs/firebaseMessaging/index.js.map +1 -1
  74. package/dist/module/utils/push/libs/firebaseMessaging/lib.js +1 -1
  75. package/dist/module/utils/push/libs/firebaseMessaging/lib.js.map +1 -1
  76. package/dist/module/utils/push/setupAndroidPushEvents.js +21 -0
  77. package/dist/module/utils/push/setupAndroidPushEvents.js.map +1 -0
  78. package/dist/module/utils/push/setupCallingExpEvents.js +6 -2
  79. package/dist/module/utils/push/setupCallingExpEvents.js.map +1 -1
  80. package/dist/module/utils/push/setupIosVoipPushEvents.js +6 -7
  81. package/dist/module/utils/push/setupIosVoipPushEvents.js.map +1 -1
  82. package/dist/module/utils/push/utils.js.map +1 -1
  83. package/dist/module/version.js +1 -1
  84. package/dist/typescript/components/Call/Lobby/Lobby.d.ts.map +1 -1
  85. package/dist/typescript/components/Call/Lobby/LobbyCameraPreview.d.ts +28 -0
  86. package/dist/typescript/components/Call/Lobby/LobbyCameraPreview.d.ts.map +1 -0
  87. package/dist/typescript/components/Call/Lobby/index.d.ts +1 -0
  88. package/dist/typescript/components/Call/Lobby/index.d.ts.map +1 -1
  89. package/dist/typescript/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.d.ts +11 -6
  90. package/dist/typescript/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.d.ts.map +1 -1
  91. package/dist/typescript/components/Participant/ParticipantView/VideoRenderer/index.d.ts.map +1 -1
  92. package/dist/typescript/hooks/push/useCallingExpWithCallingStateEffect.d.ts.map +1 -1
  93. package/dist/typescript/hooks/useLoopbackRecording.d.ts +12 -69
  94. package/dist/typescript/hooks/useLoopbackRecording.d.ts.map +1 -1
  95. package/dist/typescript/modules/call-manager/CallManager.d.ts +28 -6
  96. package/dist/typescript/modules/call-manager/CallManager.d.ts.map +1 -1
  97. package/dist/typescript/modules/call-manager/types.d.ts +0 -1
  98. package/dist/typescript/modules/call-manager/types.d.ts.map +1 -1
  99. package/dist/typescript/utils/StreamVideoRN/index.d.ts.map +1 -1
  100. package/dist/typescript/utils/StreamVideoRN/types.d.ts +3 -0
  101. package/dist/typescript/utils/StreamVideoRN/types.d.ts.map +1 -1
  102. package/dist/typescript/utils/internal/callingx/callingx.d.ts +2 -0
  103. package/dist/typescript/utils/internal/callingx/callingx.d.ts.map +1 -1
  104. package/dist/typescript/utils/internal/registerMediaEngine.d.ts +15 -0
  105. package/dist/typescript/utils/internal/registerMediaEngine.d.ts.map +1 -0
  106. package/dist/typescript/utils/internal/registerSDKGlobals.d.ts.map +1 -1
  107. package/dist/typescript/utils/push/android.d.ts +3 -4
  108. package/dist/typescript/utils/push/android.d.ts.map +1 -1
  109. package/dist/typescript/utils/push/internal/android.d.ts +8 -0
  110. package/dist/typescript/utils/push/internal/android.d.ts.map +1 -0
  111. package/dist/typescript/utils/push/libs/firebaseMessaging/index.d.ts +6 -5
  112. package/dist/typescript/utils/push/libs/firebaseMessaging/index.d.ts.map +1 -1
  113. package/dist/typescript/utils/push/libs/firebaseMessaging/lib.d.ts +2 -2
  114. package/dist/typescript/utils/push/libs/firebaseMessaging/lib.d.ts.map +1 -1
  115. package/dist/typescript/utils/push/setupAndroidPushEvents.d.ts +3 -0
  116. package/dist/typescript/utils/push/setupAndroidPushEvents.d.ts.map +1 -0
  117. package/dist/typescript/utils/push/setupCallingExpEvents.d.ts.map +1 -1
  118. package/dist/typescript/utils/push/setupIosVoipPushEvents.d.ts.map +1 -1
  119. package/dist/typescript/utils/push/utils.d.ts +2 -2
  120. package/dist/typescript/utils/push/utils.d.ts.map +1 -1
  121. package/dist/typescript/version.d.ts +1 -1
  122. package/ios/StreamInCallManager.m +4 -0
  123. package/ios/StreamInCallManager.swift +42 -10
  124. package/ios/TracksRecorder/RecorderVideoSink.swift +92 -5
  125. package/package.json +8 -8
  126. package/src/components/Call/CallControls/internal/ReactionsPicker.tsx +2 -2
  127. package/src/components/Call/Lobby/Lobby.tsx +4 -11
  128. package/src/components/Call/Lobby/LobbyCameraPreview.tsx +99 -0
  129. package/src/components/Call/Lobby/index.ts +1 -0
  130. package/src/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.tsx +56 -75
  131. package/src/components/Participant/ParticipantView/VideoRenderer/index.tsx +18 -7
  132. package/src/hooks/push/useCallingExpWithCallingStateEffect.ts +12 -5
  133. package/src/hooks/useLoopbackRecording.ts +36 -248
  134. package/src/modules/call-manager/CallManager.ts +40 -71
  135. package/src/modules/call-manager/native-module.d.ts +13 -0
  136. package/src/modules/call-manager/types.ts +0 -1
  137. package/src/utils/StreamVideoRN/index.ts +2 -0
  138. package/src/utils/StreamVideoRN/types.ts +3 -0
  139. package/src/utils/internal/callingx/callingx.ts +37 -46
  140. package/src/utils/internal/registerMediaEngine.ts +42 -0
  141. package/src/utils/internal/registerSDKGlobals.ts +115 -42
  142. package/src/utils/push/android.ts +20 -253
  143. package/src/utils/push/internal/android.ts +252 -0
  144. package/src/utils/push/libs/firebaseMessaging/index.ts +14 -22
  145. package/src/utils/push/libs/firebaseMessaging/lib.ts +3 -3
  146. package/src/utils/push/setupAndroidPushEvents.ts +25 -0
  147. package/src/utils/push/setupCallingExpEvents.ts +6 -3
  148. package/src/utils/push/setupIosVoipPushEvents.ts +8 -7
  149. package/src/utils/push/utils.ts +2 -4
  150. package/src/version.ts +1 -1
@@ -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
@@ -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? {
@@ -12,11 +12,21 @@ import WebRTC
12
12
  ///
13
13
  /// Each delivered `RTCVideoFrame` is normalised to a CVPixelBuffer in the
14
14
  /// hardware H.264 encoder's native format — **NV12**
15
- /// (`kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange`).
16
- /// `RTCCVPixelBuffer` sources (camera passthrough) are forwarded with
17
- /// the underlying pixel buffer unchanged. `RTCI420Buffer` and other YUV
18
- /// sources are converted into a fresh IOSurface-backed NV12 buffer via
19
- /// a plane reorder (no color-space conversion required).
15
+ /// (`kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange`). One branch per
16
+ /// buffer type `renderFrame` can be handed:
17
+ ///
18
+ /// - `RTCCVPixelBuffer` (camera passthrough) already wraps a CVPixelBuffer,
19
+ /// so the underlying buffer is forwarded unchanged.
20
+ /// - `RTCNV12Buffer` already holds NV12 planes, but as raw pointers rather
21
+ /// than a CVPixelBuffer, so the planes are copied row-wise into a fresh
22
+ /// one. No format conversion. This is the buffer type decoded remote
23
+ /// frames arrive as, so it is the hot path for loopback recording — do
24
+ /// not remove it. Without it these frames fall through to `toI420()`
25
+ /// below, which converts NV12 → I420 → NV12 and caps throughput at
26
+ /// roughly half the source frame rate.
27
+ /// - `RTCI420Buffer` (and anything else, via `toI420()`) is converted into
28
+ /// a fresh IOSurface-backed NV12 buffer by interleaving the U and V
29
+ /// planes — a plane reorder, no colour-space conversion.
20
30
  ///
21
31
  /// **Why NV12 and not BGRA?** AVAssetWriter's hardware encoder accepts
22
32
  /// both, but BGRA requires an internal colour-space conversion that
@@ -51,6 +61,8 @@ import WebRTC
51
61
  if let cvBuffer = frame.buffer as? RTCCVPixelBuffer {
52
62
  // Camera passthrough — already a CVPixelBuffer (typically NV12 on iOS).
53
63
  pixelBuffer = cvBuffer.pixelBuffer
64
+ } else if let nv12 = frame.buffer as? RTCNV12Buffer {
65
+ pixelBuffer = Self.makeNV12PixelBuffer(fromNV12: nv12)
54
66
  } else if let i420 = frame.buffer as? RTCI420Buffer {
55
67
  pixelBuffer = Self.makeNV12PixelBuffer(fromI420: i420)
56
68
  } else {
@@ -67,6 +79,81 @@ import WebRTC
67
79
  frameHandler(outputBuffer, frame.width, frame.height, frame.timeStampNs)
68
80
  }
69
81
 
82
+ private static func makeNV12PixelBuffer(fromNV12 nv12: RTCNV12Buffer) -> CVPixelBuffer? {
83
+ let width = Int(nv12.width)
84
+ let height = Int(nv12.height)
85
+ let chromaWidth = Int(nv12.chromaWidth)
86
+ let chromaHeight = Int(nv12.chromaHeight)
87
+
88
+ guard let buffer = makePixelBuffer(width: width, height: height) else { return nil }
89
+
90
+ CVPixelBufferLockBaseAddress(buffer, [])
91
+ defer { CVPixelBufferUnlockBaseAddress(buffer, []) }
92
+
93
+ guard let yDest = CVPixelBufferGetBaseAddressOfPlane(buffer, 0) else { return nil }
94
+ copyPlane(
95
+ source: UnsafeRawPointer(nv12.dataY),
96
+ sourceStride: Int(nv12.strideY),
97
+ destination: yDest,
98
+ destinationStride: CVPixelBufferGetBytesPerRowOfPlane(buffer, 0),
99
+ rowBytes: width,
100
+ rows: height
101
+ )
102
+
103
+ // Plane 1: UV — already interleaved, so two bytes per chroma sample.
104
+ guard let uvDest = CVPixelBufferGetBaseAddressOfPlane(buffer, 1) else { return nil }
105
+ copyPlane(
106
+ source: UnsafeRawPointer(nv12.dataUV),
107
+ sourceStride: Int(nv12.strideUV),
108
+ destination: uvDest,
109
+ destinationStride: CVPixelBufferGetBytesPerRowOfPlane(buffer, 1),
110
+ rowBytes: chromaWidth * 2,
111
+ rows: chromaHeight
112
+ )
113
+
114
+ return buffer
115
+ }
116
+
117
+ private static func makePixelBuffer(width: Int, height: Int) -> CVPixelBuffer? {
118
+ var pixelBuffer: CVPixelBuffer?
119
+ let attrs: [String: Any] = [
120
+ kCVPixelBufferIOSurfacePropertiesKey as String: [:] as [String: Any],
121
+ ]
122
+ let status = CVPixelBufferCreate(
123
+ kCFAllocatorDefault,
124
+ width,
125
+ height,
126
+ kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
127
+ attrs as CFDictionary,
128
+ &pixelBuffer
129
+ )
130
+ guard status == kCVReturnSuccess else { return nil }
131
+ return pixelBuffer
132
+ }
133
+
134
+ /// Row-wise copy, collapsing to a single `memcpy` when the strides match.
135
+ private static func copyPlane(
136
+ source: UnsafeRawPointer,
137
+ sourceStride: Int,
138
+ destination: UnsafeMutableRawPointer,
139
+ destinationStride: Int,
140
+ rowBytes: Int,
141
+ rows: Int
142
+ ) {
143
+ if sourceStride == destinationStride && sourceStride == rowBytes {
144
+ memcpy(destination, source, rowBytes * rows)
145
+ return
146
+ }
147
+ let copyBytes = min(rowBytes, min(sourceStride, destinationStride))
148
+ for row in 0..<rows {
149
+ memcpy(
150
+ destination.advanced(by: row * destinationStride),
151
+ source.advanced(by: row * sourceStride),
152
+ copyBytes
153
+ )
154
+ }
155
+ }
156
+
70
157
  // MARK: - I420 → NV12
71
158
 
72
159
  /// Allocates a fresh IOSurface-backed NV12 `CVPixelBuffer` and copies
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-react-native-sdk",
3
- "version": "1.43.0",
3
+ "version": "1.45.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/",
@@ -52,8 +52,8 @@
52
52
  "!**/.*"
53
53
  ],
54
54
  "dependencies": {
55
- "@stream-io/video-client": "1.57.0",
56
- "@stream-io/video-react-bindings": "1.19.2",
55
+ "@stream-io/video-client": "1.59.0",
56
+ "@stream-io/video-react-bindings": "1.20.1",
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.2.0",
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": "*",
@@ -117,11 +117,11 @@
117
117
  "@react-native/babel-preset": "0.86.2",
118
118
  "@react-native/jest-preset": "0.86.2",
119
119
  "@react-native/metro-config": "0.86.2",
120
- "@stream-io/noise-cancellation-react-native": "^0.10.0",
121
- "@stream-io/react-native-callingx": "^0.9.0",
122
- "@stream-io/react-native-webrtc": "145.2.0",
120
+ "@stream-io/noise-cancellation-react-native": "^0.11.0",
121
+ "@stream-io/react-native-callingx": "^0.11.0",
122
+ "@stream-io/react-native-webrtc": "145.3.1",
123
123
  "@stream-io/typescript-config": "^0.0.0",
124
- "@stream-io/video-filters-react-native": "^0.15.0",
124
+ "@stream-io/video-filters-react-native": "^0.16.0",
125
125
  "@testing-library/jest-native": "^5.4.3",
126
126
  "@testing-library/react-native": "13.3.3",
127
127
  "@tsconfig/node18": "^18.2.6",
@@ -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';
@@ -1,5 +1,4 @@
1
- import { forwardRef, useImperativeHandle, useEffect, useMemo } from 'react';
2
- import { LayoutChangeEvent } from 'react-native';
1
+ import { useEffect } from 'react';
3
2
  import {
4
3
  Call,
5
4
  CallingState,
@@ -18,15 +17,17 @@ import {
18
17
  filter,
19
18
  map,
20
19
  } from 'rxjs';
21
- export type TrackSubscriberHandle = {
22
- onLayoutUpdate: (event: LayoutChangeEvent) => void;
23
- };
24
20
 
25
21
  type TrackSubscriberProps = {
26
22
  participantSessionId: string;
27
23
  call: Call;
28
24
  trackType: VideoTrackType;
29
25
  isVisible: boolean;
26
+ /**
27
+ * The dimensions of the view rendering the track, owned by the parent so that
28
+ * the last reported layout survives a remount of this component.
29
+ */
30
+ dimensions$: BehaviorSubject<SfuModels.VideoDimension | undefined>;
30
31
  };
31
32
 
32
33
  /**
@@ -40,81 +41,61 @@ type TrackSubscriberProps = {
40
41
  * 1. When the participant stops publishing the video track
41
42
  * 2. When the participant becomes invisible
42
43
  */
43
- const TrackSubscriber = forwardRef<TrackSubscriberHandle, TrackSubscriberProps>(
44
- (props, ref) => {
45
- const { call, participantSessionId, trackType, isVisible } = props;
46
- const dimensions$ = useMemo(() => {
47
- return new BehaviorSubject<SfuModels.VideoDimension | undefined>(
48
- undefined,
49
- );
50
- }, []);
51
-
52
- useEffect(() => {
53
- const requestTrackWithDimensions = (
54
- debounceType: DebounceType,
55
- dimension: SfuModels.VideoDimension | undefined,
56
- ) => {
57
- if (dimension && (dimension.width === 0 || dimension.height === 0)) {
58
- // ignore 0x0 dimensions. this can happen when the video element
59
- // is not visible (e.g., has display: none).
60
- // we treat this as "unsubscription" as we don't want to keep
61
- // consuming bandwidth for a video that is not visible on the screen.
62
- dimension = undefined;
63
- }
64
- call.state.updateParticipantTracks(trackType, {
65
- [participantSessionId]: { dimension },
66
- });
67
- call.trackSubscriptionManager.apply(debounceType);
68
- };
69
- const isPublishingTrack$ = call.state.participants$.pipe(
70
- map((ps) => ps.find((p) => p.sessionId === participantSessionId)),
71
- filter((p): p is StreamVideoParticipant => !!p),
72
- distinctUntilKeyChanged('publishedTracks'),
73
- map((p) =>
74
- trackType === 'videoTrack' ? hasVideo(p) : hasScreenShare(p),
75
- ),
76
- distinctUntilChanged(),
77
- );
78
- const isJoinedState$ = call.state.callingState$.pipe(
79
- map((callingState) => callingState === CallingState.JOINED),
80
- );
44
+ const TrackSubscriber = (props: TrackSubscriberProps) => {
45
+ const { call, participantSessionId, trackType, isVisible, dimensions$ } =
46
+ props;
81
47
 
82
- const subscription = combineLatest([
83
- dimensions$,
84
- isPublishingTrack$,
85
- isJoinedState$,
86
- ]).subscribe(([dimension, isPublishing, isJoined]) => {
87
- if (isJoined) {
88
- if (!isVisible || !isPublishing) {
89
- requestTrackWithDimensions(DebounceType.MEDIUM, undefined);
90
- } else if (dimension) {
91
- requestTrackWithDimensions(DebounceType.IMMEDIATE, dimension);
92
- }
93
- }
48
+ useEffect(() => {
49
+ const requestTrackWithDimensions = (
50
+ debounceType: DebounceType,
51
+ dimension: SfuModels.VideoDimension | undefined,
52
+ ) => {
53
+ if (dimension && (dimension.width === 0 || dimension.height === 0)) {
54
+ // ignore 0x0 dimensions. this can happen when the video element
55
+ // is not visible (e.g., has display: none).
56
+ // we treat this as "unsubscription" as we don't want to keep
57
+ // consuming bandwidth for a video that is not visible on the screen.
58
+ dimension = undefined;
59
+ }
60
+ call.state.updateParticipantTracks(trackType, {
61
+ [participantSessionId]: { dimension },
94
62
  });
63
+ call.trackSubscriptionManager.apply(debounceType);
64
+ };
65
+ const isPublishingTrack$ = call.state.participants$.pipe(
66
+ map((ps) => ps.find((p) => p.sessionId === participantSessionId)),
67
+ filter((p): p is StreamVideoParticipant => !!p),
68
+ distinctUntilKeyChanged('publishedTracks'),
69
+ map((p) =>
70
+ trackType === 'videoTrack' ? hasVideo(p) : hasScreenShare(p),
71
+ ),
72
+ distinctUntilChanged(),
73
+ );
74
+ const isJoinedState$ = call.state.callingState$.pipe(
75
+ map((callingState) => callingState === CallingState.JOINED),
76
+ );
95
77
 
96
- return () => {
97
- subscription.unsubscribe();
98
- };
99
- }, [call, participantSessionId, trackType, isVisible, dimensions$]);
78
+ const subscription = combineLatest([
79
+ dimensions$,
80
+ isPublishingTrack$,
81
+ isJoinedState$,
82
+ ]).subscribe(([dimension, isPublishing, isJoined]) => {
83
+ if (isJoined) {
84
+ if (!isVisible || !isPublishing) {
85
+ requestTrackWithDimensions(DebounceType.MEDIUM, undefined);
86
+ } else if (dimension) {
87
+ requestTrackWithDimensions(DebounceType.IMMEDIATE, dimension);
88
+ }
89
+ }
90
+ });
100
91
 
101
- useImperativeHandle(
102
- ref,
103
- () => ({
104
- onLayoutUpdate: (event) => {
105
- const dimension = {
106
- width: Math.trunc(event.nativeEvent.layout.width),
107
- height: Math.trunc(event.nativeEvent.layout.height),
108
- };
109
- dimensions$.next(dimension);
110
- },
111
- }),
112
- [dimensions$],
113
- );
92
+ return () => {
93
+ subscription.unsubscribe();
94
+ };
95
+ }, [call, participantSessionId, trackType, isVisible, dimensions$]);
114
96
 
115
- return null;
116
- },
117
- );
97
+ return null;
98
+ };
118
99
 
119
100
  TrackSubscriber.displayName = 'TrackSubscriber';
120
101
 
@@ -1,5 +1,5 @@
1
- import React, { useEffect, useRef } from 'react';
2
- import { Platform, StyleSheet, View } from 'react-native';
1
+ import React, { useEffect, useMemo, useRef } from 'react';
2
+ import { LayoutChangeEvent, Platform, StyleSheet, View } from 'react-native';
3
3
  import type { MediaStream } from '@stream-io/react-native-webrtc';
4
4
  import { RTCView } from '@stream-io/react-native-webrtc';
5
5
  import type { ParticipantViewProps } from '../ParticipantView';
@@ -7,15 +7,17 @@ import {
7
7
  hasPausedTrack,
8
8
  hasScreenShare,
9
9
  hasVideo,
10
+ SfuModels,
10
11
  type VideoTrackType,
11
12
  VisibilityState,
12
13
  } from '@stream-io/video-client';
13
14
  import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
15
+ import { BehaviorSubject } from 'rxjs';
14
16
  import { ParticipantVideoFallback as DefaultParticipantVideoFallback } from '../ParticipantVideoFallback';
15
17
  import { useTheme } from '../../../../contexts/ThemeContext';
16
18
  import { useTrackDimensions } from '../../../../hooks/useTrackDimensions';
17
19
  import { useScreenshotIosContext } from '../../../../contexts/internal/ScreenshotIosContext';
18
- import TrackSubscriber, { TrackSubscriberHandle } from './TrackSubscriber';
20
+ import TrackSubscriber from './TrackSubscriber';
19
21
 
20
22
  const DEFAULT_VIEWPORT_VISIBILITY_STATE: Record<
21
23
  VideoTrackType,
@@ -59,7 +61,13 @@ export const VideoRenderer = React.memo(
59
61
  } = useTheme();
60
62
  const call = useCall();
61
63
  const { useCameraState, useIncomingVideoSettings } = useCallStateHooks();
62
- const trackSubscriberRef = useRef<TrackSubscriberHandle>(null);
64
+ // a sticky note of the dimensions of the RTCView from onLayout
65
+ // to reuse for a new participant even if dimensions dont change
66
+ const dimensions$ = useMemo(
67
+ () =>
68
+ new BehaviorSubject<SfuModels.VideoDimension | undefined>(undefined),
69
+ [],
70
+ );
63
71
  const { isParticipantVideoEnabled } = useIncomingVideoSettings();
64
72
  const { direction } = useCameraState();
65
73
  const viewRef = useRef(null);
@@ -199,9 +207,12 @@ export const VideoRenderer = React.memo(
199
207
  ]);
200
208
 
201
209
  const onLayout: React.ComponentProps<typeof RTCView>['onLayout'] = (
202
- event,
210
+ event: LayoutChangeEvent,
203
211
  ) => {
204
- trackSubscriberRef.current?.onLayoutUpdate(event);
212
+ dimensions$.next({
213
+ width: Math.trunc(event.nativeEvent.layout.width),
214
+ height: Math.trunc(event.nativeEvent.layout.height),
215
+ });
205
216
  };
206
217
 
207
218
  return (
@@ -211,11 +222,11 @@ export const VideoRenderer = React.memo(
211
222
  >
212
223
  {call && !isLocalParticipant && (
213
224
  <TrackSubscriber
214
- ref={trackSubscriberRef}
215
225
  call={call}
216
226
  participantSessionId={sessionId}
217
227
  trackType={trackType}
218
228
  isVisible={isVisible}
229
+ dimensions$={dimensions$}
219
230
  />
220
231
  )}
221
232
  {canShowVideo &&