@stream-io/video-react-native-sdk 1.44.0 → 2.0.0-beta.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 (106) hide show
  1. package/CHANGELOG.md +26 -3
  2. package/android/src/main/java/com/streamvideo/reactnative/keepalive/StreamCallKeepAliveHeadlessService.kt +112 -26
  3. package/android/src/main/java/com/streamvideo/reactnative/util/CallAlivePermissionsHelper.kt +68 -37
  4. package/dist/commonjs/hooks/push/useCallingExpWithCallingStateEffect.js +12 -6
  5. package/dist/commonjs/hooks/push/useCallingExpWithCallingStateEffect.js.map +1 -1
  6. package/dist/commonjs/hooks/useAndroidKeepCallAliveEffect.js +38 -11
  7. package/dist/commonjs/hooks/useAndroidKeepCallAliveEffect.js.map +1 -1
  8. package/dist/commonjs/hooks/useLoopbackRecording.js +10 -127
  9. package/dist/commonjs/hooks/useLoopbackRecording.js.map +1 -1
  10. package/dist/commonjs/utils/StreamVideoRN/index.js +2 -0
  11. package/dist/commonjs/utils/StreamVideoRN/index.js.map +1 -1
  12. package/dist/commonjs/utils/internal/callingx/callingx.js +38 -56
  13. package/dist/commonjs/utils/internal/callingx/callingx.js.map +1 -1
  14. package/dist/commonjs/utils/keepCallAliveHeadlessTask.js +43 -4
  15. package/dist/commonjs/utils/keepCallAliveHeadlessTask.js.map +1 -1
  16. package/dist/commonjs/utils/push/android.js +7 -167
  17. package/dist/commonjs/utils/push/android.js.map +1 -1
  18. package/dist/commonjs/utils/push/internal/android.js +204 -0
  19. package/dist/commonjs/utils/push/internal/android.js.map +1 -0
  20. package/dist/commonjs/utils/push/libs/firebaseMessaging/index.js +9 -1
  21. package/dist/commonjs/utils/push/libs/firebaseMessaging/index.js.map +1 -1
  22. package/dist/commonjs/utils/push/libs/firebaseMessaging/lib.js +1 -1
  23. package/dist/commonjs/utils/push/libs/firebaseMessaging/lib.js.map +1 -1
  24. package/dist/commonjs/utils/push/setupAndroidPushEvents.js +25 -0
  25. package/dist/commonjs/utils/push/setupAndroidPushEvents.js.map +1 -0
  26. package/dist/commonjs/utils/push/setupCallingExpEvents.js +6 -2
  27. package/dist/commonjs/utils/push/setupCallingExpEvents.js.map +1 -1
  28. package/dist/commonjs/utils/push/setupIosVoipPushEvents.js +6 -7
  29. package/dist/commonjs/utils/push/setupIosVoipPushEvents.js.map +1 -1
  30. package/dist/commonjs/utils/push/utils.js.map +1 -1
  31. package/dist/commonjs/version.js +1 -1
  32. package/dist/commonjs/version.js.map +1 -1
  33. package/dist/module/hooks/push/useCallingExpWithCallingStateEffect.js +12 -6
  34. package/dist/module/hooks/push/useCallingExpWithCallingStateEffect.js.map +1 -1
  35. package/dist/module/hooks/useAndroidKeepCallAliveEffect.js +39 -12
  36. package/dist/module/hooks/useAndroidKeepCallAliveEffect.js.map +1 -1
  37. package/dist/module/hooks/useLoopbackRecording.js +10 -127
  38. package/dist/module/hooks/useLoopbackRecording.js.map +1 -1
  39. package/dist/module/utils/StreamVideoRN/index.js +2 -0
  40. package/dist/module/utils/StreamVideoRN/index.js.map +1 -1
  41. package/dist/module/utils/internal/callingx/callingx.js +38 -56
  42. package/dist/module/utils/internal/callingx/callingx.js.map +1 -1
  43. package/dist/module/utils/keepCallAliveHeadlessTask.js +41 -3
  44. package/dist/module/utils/keepCallAliveHeadlessTask.js.map +1 -1
  45. package/dist/module/utils/push/android.js +9 -169
  46. package/dist/module/utils/push/android.js.map +1 -1
  47. package/dist/module/utils/push/internal/android.js +200 -0
  48. package/dist/module/utils/push/internal/android.js.map +1 -0
  49. package/dist/module/utils/push/libs/firebaseMessaging/index.js +9 -1
  50. package/dist/module/utils/push/libs/firebaseMessaging/index.js.map +1 -1
  51. package/dist/module/utils/push/libs/firebaseMessaging/lib.js +1 -1
  52. package/dist/module/utils/push/libs/firebaseMessaging/lib.js.map +1 -1
  53. package/dist/module/utils/push/setupAndroidPushEvents.js +21 -0
  54. package/dist/module/utils/push/setupAndroidPushEvents.js.map +1 -0
  55. package/dist/module/utils/push/setupCallingExpEvents.js +6 -2
  56. package/dist/module/utils/push/setupCallingExpEvents.js.map +1 -1
  57. package/dist/module/utils/push/setupIosVoipPushEvents.js +6 -7
  58. package/dist/module/utils/push/setupIosVoipPushEvents.js.map +1 -1
  59. package/dist/module/utils/push/utils.js.map +1 -1
  60. package/dist/module/version.js +1 -1
  61. package/dist/module/version.js.map +1 -1
  62. package/dist/typescript/hooks/push/useCallingExpWithCallingStateEffect.d.ts.map +1 -1
  63. package/dist/typescript/hooks/useAndroidKeepCallAliveEffect.d.ts.map +1 -1
  64. package/dist/typescript/hooks/useLoopbackRecording.d.ts +12 -69
  65. package/dist/typescript/hooks/useLoopbackRecording.d.ts.map +1 -1
  66. package/dist/typescript/utils/StreamVideoRN/index.d.ts.map +1 -1
  67. package/dist/typescript/utils/StreamVideoRN/types.d.ts +1 -2
  68. package/dist/typescript/utils/StreamVideoRN/types.d.ts.map +1 -1
  69. package/dist/typescript/utils/internal/callingx/callingx.d.ts.map +1 -1
  70. package/dist/typescript/utils/keepCallAliveHeadlessTask.d.ts +9 -0
  71. package/dist/typescript/utils/keepCallAliveHeadlessTask.d.ts.map +1 -1
  72. package/dist/typescript/utils/push/android.d.ts +3 -4
  73. package/dist/typescript/utils/push/android.d.ts.map +1 -1
  74. package/dist/typescript/utils/push/internal/android.d.ts +8 -0
  75. package/dist/typescript/utils/push/internal/android.d.ts.map +1 -0
  76. package/dist/typescript/utils/push/libs/firebaseMessaging/index.d.ts +6 -4
  77. package/dist/typescript/utils/push/libs/firebaseMessaging/index.d.ts.map +1 -1
  78. package/dist/typescript/utils/push/libs/firebaseMessaging/lib.d.ts +2 -2
  79. package/dist/typescript/utils/push/libs/firebaseMessaging/lib.d.ts.map +1 -1
  80. package/dist/typescript/utils/push/setupAndroidPushEvents.d.ts +3 -0
  81. package/dist/typescript/utils/push/setupAndroidPushEvents.d.ts.map +1 -0
  82. package/dist/typescript/utils/push/setupCallingExpEvents.d.ts.map +1 -1
  83. package/dist/typescript/utils/push/setupIosVoipPushEvents.d.ts.map +1 -1
  84. package/dist/typescript/utils/push/utils.d.ts +2 -2
  85. package/dist/typescript/utils/push/utils.d.ts.map +1 -1
  86. package/dist/typescript/version.d.ts +1 -1
  87. package/dist/typescript/version.d.ts.map +1 -1
  88. package/expo-config-plugin/dist/withAndroidPermissions.js +1 -1
  89. package/ios/TracksRecorder/RecorderVideoSink.swift +92 -5
  90. package/package.json +4 -4
  91. package/src/hooks/push/useCallingExpWithCallingStateEffect.ts +25 -11
  92. package/src/hooks/useAndroidKeepCallAliveEffect.ts +59 -21
  93. package/src/hooks/useLoopbackRecording.ts +36 -248
  94. package/src/utils/StreamVideoRN/index.ts +2 -0
  95. package/src/utils/StreamVideoRN/types.ts +1 -2
  96. package/src/utils/internal/callingx/callingx.ts +49 -68
  97. package/src/utils/keepCallAliveHeadlessTask.ts +45 -3
  98. package/src/utils/push/android.ts +12 -237
  99. package/src/utils/push/internal/android.ts +273 -0
  100. package/src/utils/push/libs/firebaseMessaging/index.ts +16 -5
  101. package/src/utils/push/libs/firebaseMessaging/lib.ts +3 -3
  102. package/src/utils/push/setupAndroidPushEvents.ts +25 -0
  103. package/src/utils/push/setupCallingExpEvents.ts +6 -3
  104. package/src/utils/push/setupIosVoipPushEvents.ts +8 -7
  105. package/src/utils/push/utils.ts +2 -4
  106. package/src/version.ts +1 -1
@@ -1,5 +1,7 @@
1
- import { type Type } from './lib';
2
- export type { FirebaseMessagingTypes } from '@react-native-firebase/messaging';
3
- export type FirebaseMessagingType = Type;
4
- export declare function getFirebaseMessagingLib(): FirebaseMessagingType;
1
+ import { type MessagingModule } from './lib';
2
+ export type FirebaseRemoteMessage = NonNullable<Awaited<ReturnType<MessagingModule['getInitialNotification']>>>;
3
+ export declare function getFirebaseMessagingLib(): () => {
4
+ getToken: () => Promise<string>;
5
+ onTokenRefresh: (listener: (token: string) => void) => () => void;
6
+ };
5
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../src/utils/push/libs/firebaseMessaging/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,IAAI,EAAE,MAAM,OAAO,CAAC;AAEvC,YAAY,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC/E,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAKzC,wBAAgB,uBAAuB,IAAI,qBAAqB,CAQ/D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../src/utils/push/libs/firebaseMessaging/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,eAAe,EAAE,MAAM,OAAO,CAAC;AAKlD,MAAM,MAAM,qBAAqB,GAAG,WAAW,CAC7C,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAC/D,CAAC;AAYF,wBAAgB,uBAAuB;;+BAKR,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI;EAGrD"}
@@ -1,4 +1,4 @@
1
- export type Type = typeof import('@react-native-firebase/messaging').default;
2
- declare let lib: Type | undefined;
1
+ export type MessagingModule = typeof import('@react-native-firebase/messaging');
2
+ declare let lib: MessagingModule | undefined;
3
3
  export { lib };
4
4
  //# sourceMappingURL=lib.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../../../../../../src/utils/push/libs/firebaseMessaging/lib.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,IAAI,GAAG,cAAc,kCAAkC,EAAE,OAAO,CAAC;AAE7E,QAAA,IAAI,GAAG,EAAE,IAAI,GAAG,SAAS,CAAC;AAM1B,OAAO,EAAE,GAAG,EAAE,CAAC"}
1
+ {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../../../../../../src/utils/push/libs/firebaseMessaging/lib.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG,cAAc,kCAAkC,CAAC,CAAC;AAEhF,QAAA,IAAI,GAAG,EAAE,eAAe,GAAG,SAAS,CAAC;AAMrC,OAAO,EAAE,GAAG,EAAE,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { StreamVideoConfig } from '../StreamVideoRN/types';
2
+ export declare function setupAndroidPushEvents(pushConfig: NonNullable<StreamVideoConfig['push']>): void;
3
+ //# sourceMappingURL=setupAndroidPushEvents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setupAndroidPushEvents.d.ts","sourceRoot":"","sources":["../../../../src/utils/push/setupAndroidPushEvents.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAI3D,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,WAAW,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,QAiBnD"}
@@ -1 +1 @@
1
- {"version":3,"file":"setupCallingExpEvents.d.ts","sourceRoot":"","sources":["../../../../src/utils/push/setupCallingExpEvents.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAYhE,KAAK,UAAU,GAAG,WAAW,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC;AAIzD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,WAAW,CAAC,UAAU,CAAC,QAsDxE"}
1
+ {"version":3,"file":"setupCallingExpEvents.d.ts","sourceRoot":"","sources":["../../../../src/utils/push/setupCallingExpEvents.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAahE,KAAK,UAAU,GAAG,WAAW,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC;AAIzD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,WAAW,CAAC,UAAU,CAAC,QAyDxE"}
@@ -1 +1 @@
1
- {"version":3,"file":"setupIosVoipPushEvents.d.ts","sourceRoot":"","sources":["../../../../src/utils/push/setupIosVoipPushEvents.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAI3D,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,WAAW,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,QAkBnD"}
1
+ {"version":3,"file":"setupIosVoipPushEvents.d.ts","sourceRoot":"","sources":["../../../../src/utils/push/setupIosVoipPushEvents.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAI3D,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,WAAW,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,QAmBnD"}
@@ -1,9 +1,9 @@
1
- import type { FirebaseMessagingTypes } from './libs/firebaseMessaging';
1
+ import type { FirebaseRemoteMessage } from './libs/firebaseMessaging';
2
2
  import type { NonRingingPushEvent } from '../StreamVideoRN/types';
3
3
  export type StreamPushPayload = {
4
4
  call_cid: string;
5
5
  type: 'call.ring' | NonRingingPushEvent;
6
6
  sender: string;
7
7
  } | undefined;
8
- export declare function isFirebaseStreamVideoMessage(message: FirebaseMessagingTypes.RemoteMessage): boolean;
8
+ export declare function isFirebaseStreamVideoMessage(message: FirebaseRemoteMessage): boolean;
9
9
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../src/utils/push/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AACvE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAElE,MAAM,MAAM,iBAAiB,GACzB;IACE,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,WAAW,GAAG,mBAAmB,CAAC;IACxC,MAAM,EAAE,MAAM,CAAC;CAChB,GACD,SAAS,CAAC;AAEd,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,sBAAsB,CAAC,aAAa,WAG9C"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../src/utils/push/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAElE,MAAM,MAAM,iBAAiB,GACzB;IACE,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,WAAW,GAAG,mBAAmB,CAAC;IACxC,MAAM,EAAE,MAAM,CAAC;CAChB,GACD,SAAS,CAAC;AAEd,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,qBAAqB,WAE1E"}
@@ -1,2 +1,2 @@
1
- export declare const version = "1.44.0";
1
+ export declare const version = "2.0.0-beta.0";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,WAAW,CAAC"}
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,iBAAiB,CAAC"}
@@ -15,7 +15,7 @@ const withStreamVideoReactNativeSDKAndroidPermissions = (configuration, props) =
15
15
  }
16
16
  }
17
17
  if (props?.androidKeepCallAlive) {
18
- permissions.push('android.permission.FOREGROUND_SERVICE_CAMERA', 'android.permission.FOREGROUND_SERVICE_MICROPHONE', 'android.permission.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK');
18
+ permissions.push('android.permission.FOREGROUND_SERVICE_CAMERA', 'android.permission.FOREGROUND_SERVICE_MICROPHONE', 'android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK');
19
19
  }
20
20
  const config = config_plugins_1.AndroidConfig.Permissions.withPermissions(configuration, permissions);
21
21
  return config;
@@ -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.44.0",
3
+ "version": "2.0.0-beta.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.58.0",
56
- "@stream-io/video-react-bindings": "1.20.0",
55
+ "@stream-io/video-client": "2.0.0-beta.0",
56
+ "@stream-io/video-react-bindings": "2.0.0-beta.0",
57
57
  "intl-pluralrules": "2.0.1",
58
58
  "react-native-url-polyfill": "^3.0.0",
59
59
  "rxjs": "~7.8.2",
@@ -118,7 +118,7 @@
118
118
  "@react-native/jest-preset": "0.86.2",
119
119
  "@react-native/metro-config": "0.86.2",
120
120
  "@stream-io/noise-cancellation-react-native": "^0.11.0",
121
- "@stream-io/react-native-callingx": "^0.10.0",
121
+ "@stream-io/react-native-callingx": "^0.11.1",
122
122
  "@stream-io/react-native-webrtc": "145.3.1",
123
123
  "@stream-io/typescript-config": "^0.0.0",
124
124
  "@stream-io/video-filters-react-native": "^0.16.0",
@@ -17,7 +17,7 @@ export const useCallingExpWithCallingStateEffect = () => {
17
17
  useCallStateHooks();
18
18
 
19
19
  const activeCall = useCall();
20
- const { isMute, microphone } = useMicrophoneState();
20
+ const { optimisticStatus, microphone } = useMicrophoneState();
21
21
  const callMembers = useCallMembers();
22
22
  const participants = useParticipants();
23
23
 
@@ -30,7 +30,7 @@ export const useCallingExpWithCallingStateEffect = () => {
30
30
 
31
31
  const callDisplayName = useMemo(
32
32
  () =>
33
- callCustomDisplayName ??
33
+ callCustomDisplayName ||
34
34
  getCallDisplayName(callMembers, participants, currentUserId),
35
35
  [callMembers, participants, currentUserId, callCustomDisplayName],
36
36
  );
@@ -112,12 +112,19 @@ export const useCallingExpWithCallingStateEffect = () => {
112
112
  return;
113
113
  }
114
114
 
115
- callingx.updateDisplay(
116
- activeCallCid,
117
- createdByUserId ?? callDisplayName,
118
- callDisplayName,
119
- isIncoming,
120
- );
115
+ callingx
116
+ .updateDisplay(
117
+ activeCallCid,
118
+ createdByUserId ?? callDisplayName,
119
+ callDisplayName,
120
+ isIncoming,
121
+ )
122
+ .catch((error: unknown) => {
123
+ logger.debug(
124
+ `useCallingExpWithCallingStateEffect: Error updating display in callingx: ${activeCallCid}`,
125
+ error,
126
+ );
127
+ });
121
128
  }, [activeCallCid, createdByUserId, callDisplayName, isIncoming]);
122
129
 
123
130
  // Sync microphone mute state from app → CallKit
@@ -135,8 +142,14 @@ export const useCallingExpWithCallingStateEffect = () => {
135
142
  return;
136
143
  }
137
144
 
138
- callingx.setMutedCall(activeCallCid, isMute);
139
- }, [activeCallCid, isMute]);
145
+ if (optimisticStatus === undefined) {
146
+ logger.debug(
147
+ `useCallingExpWithCallingStateEffect: mic status is not resolved yet, skipping mute sync for: ${activeCallCid}`,
148
+ );
149
+ return;
150
+ }
151
+ callingx.setMutedCall(activeCallCid, optimisticStatus === 'disabled');
152
+ }, [activeCallCid, optimisticStatus]);
140
153
 
141
154
  // Sync mute state from CallKit → app (only for system-initiated mute actions)
142
155
  useEffect(() => {
@@ -165,7 +178,8 @@ export const useCallingExpWithCallingStateEffect = () => {
165
178
  return;
166
179
  }
167
180
 
168
- const isCurrentlyMuted = microphone.state.status === 'disabled';
181
+ const isCurrentlyMuted =
182
+ microphone.state.optimisticStatus === 'disabled';
169
183
  if (isCurrentlyMuted === muted) {
170
184
  logger.debug(
171
185
  `useCallingExpWithCallingStateEffect: Mic toggle is already in the desired state: ${muted} for call: ${activeCallCid}`,
@@ -9,7 +9,10 @@ import {
9
9
  Platform,
10
10
  } from 'react-native';
11
11
  import { CallingState, videoLoggerSystem } from '@stream-io/video-client';
12
- import { keepCallAliveCallRef } from '../utils/keepCallAliveHeadlessTask';
12
+ import {
13
+ endKeepCallAliveHeadlessTask,
14
+ keepCallAliveCallRef,
15
+ } from '../utils/keepCallAliveHeadlessTask';
13
16
  import { getCallingxLibIfAvailable } from '../utils/push/libs';
14
17
 
15
18
  async function stopForegroundServiceNoThrow() {
@@ -21,7 +24,10 @@ async function stopForegroundServiceNoThrow() {
21
24
  }
22
25
  }
23
26
 
24
- async function startForegroundService(call_cid: string) {
27
+ /**
28
+ * @returns true when the native foreground service was asked to start.
29
+ */
30
+ async function startForegroundService(call_cid: string): Promise<boolean> {
25
31
  const logger = videoLoggerSystem.getLogger('startForegroundService');
26
32
  const isCallAliveConfigured = await (async () => {
27
33
  try {
@@ -32,10 +38,14 @@ async function startForegroundService(call_cid: string) {
32
38
  }
33
39
  })();
34
40
  if (!isCallAliveConfigured) {
35
- logger.info(
36
- 'KeepCallAlive is not configured. Skipping foreground service setup.',
41
+ logger.warn(
42
+ 'KeepCallAlive is not configured, the call will not survive the app going to the ' +
43
+ 'background: audio publishing stops and the user is eventually dropped from the call. ' +
44
+ 'Declare android.permission.FOREGROUND_SERVICE and ' +
45
+ 'android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK in the app manifest (see logcat, ' +
46
+ 'tag StreamVideoReactNative, for the exact list of missing permissions).',
37
47
  );
38
- return;
48
+ return false;
39
49
  }
40
50
  // Check for notification permission (Android 13+) before starting the service.
41
51
  const hasPostNotificationsPermission =
@@ -47,7 +57,7 @@ async function startForegroundService(call_cid: string) {
47
57
  logger.info(
48
58
  'Notification permission not granted, can not start foreground service to keep the call alive',
49
59
  );
50
- return;
60
+ return false;
51
61
  }
52
62
  const videoConfig = StreamVideoRN.getConfig();
53
63
  const foregroundServiceConfig = videoConfig.foregroundService;
@@ -58,19 +68,34 @@ async function startForegroundService(call_cid: string) {
58
68
  // NOTE: we use requestAnimationFrame to ensure that the foreground service is started after all the current UI operations are done
59
69
  // this is a workaround for the crash - android.app.RemoteServiceException$ForegroundServiceDidNotStartInTimeException: Context.startForegroundService() did not then call Service.startForeground()
60
70
  // this crash was reproducible only in some android devices
61
- requestAnimationFrame(async () => {
62
- try {
63
- await NativeModules.StreamVideoReactNative.startKeepCallAliveService(
64
- call_cid,
65
- channel.id,
66
- channel.name,
67
- notificationTexts.title,
68
- notificationTexts.body,
69
- smallIconName ?? null,
70
- );
71
- } catch (e) {
72
- logger.warn('Failed to start keep-call-alive foreground service', e);
73
- }
71
+ return new Promise<boolean>((resolve) => {
72
+ requestAnimationFrame(async () => {
73
+ // The service declares the camera/microphone foreground service types, which are
74
+ // while-in-use permissions: Android requires startForegroundService() to be called while the
75
+ // app has a visible activity, otherwise the start is rejected. The frame we deferred to can
76
+ // land after the app left the foreground, so re-check before calling native.
77
+ if (AppState.currentState !== 'active') {
78
+ logger.info(
79
+ 'App is no longer in the foreground, not starting the keep-call-alive foreground service',
80
+ );
81
+ resolve(false);
82
+ return;
83
+ }
84
+ try {
85
+ await NativeModules.StreamVideoReactNative.startKeepCallAliveService(
86
+ call_cid,
87
+ channel.id,
88
+ channel.name,
89
+ notificationTexts.title,
90
+ notificationTexts.body,
91
+ smallIconName ?? null,
92
+ );
93
+ resolve(true);
94
+ } catch (e) {
95
+ logger.warn('Failed to start keep-call-alive foreground service', e);
96
+ resolve(false);
97
+ }
98
+ });
74
99
  });
75
100
  }
76
101
 
@@ -147,8 +172,18 @@ export const useAndroidKeepCallAliveEffect = () => {
147
172
  return;
148
173
  }
149
174
 
150
- await startForegroundService(activeCallCid);
151
- foregroundServiceStartedRef.current = true;
175
+ try {
176
+ // only mark as started when native actually accepted the start, so that a later render
177
+ // while still joined can retry.
178
+ foregroundServiceStartedRef.current =
179
+ await startForegroundService(activeCallCid);
180
+ } catch (e) {
181
+ // run() is intentionally not awaited, so anything thrown here would surface as an
182
+ // unhandled rejection. Leave the ref false so a later render can retry.
183
+ videoLoggerSystem
184
+ .getLogger('useAndroidKeepCallAliveEffect')
185
+ .warn('Failed to start the keep-call-alive foreground service', e);
186
+ }
152
187
  };
153
188
 
154
189
  // ensure that app is active before running the function
@@ -170,6 +205,8 @@ export const useAndroidKeepCallAliveEffect = () => {
170
205
  } else if (isCallEnded) {
171
206
  if (foregroundServiceStartedRef.current) {
172
207
  keepCallAliveCallRef.current = undefined;
208
+ // let the headless task resolve so RN unwinds it, then stop the foreground service
209
+ endKeepCallAliveHeadlessTask();
173
210
  // stop foreground service when the call is not active
174
211
  stopForegroundServiceNoThrow();
175
212
  foregroundServiceStartedRef.current = false;
@@ -188,6 +225,7 @@ export const useAndroidKeepCallAliveEffect = () => {
188
225
  // stop foreground service when this effect is unmounted
189
226
  if (foregroundServiceStartedRef.current) {
190
227
  keepCallAliveCallRef.current = undefined;
228
+ endKeepCallAliveHeadlessTask();
191
229
  stopForegroundServiceNoThrow();
192
230
  foregroundServiceStartedRef.current = false;
193
231
  }