@stream-io/video-react-native-sdk 1.30.5 → 1.31.1-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 (248) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/android/src/main/AndroidManifest.xml +8 -1
  3. package/android/src/main/AndroidManifestNew.xml +11 -0
  4. package/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt +127 -5
  5. package/android/src/main/java/com/streamvideo/reactnative/audio/utils/WebRtcAudioUtils.kt +70 -6
  6. package/android/src/main/java/com/streamvideo/reactnative/callmanager/StreamInCallManagerModule.kt +6 -4
  7. package/android/src/main/java/com/streamvideo/reactnative/keepalive/KeepAliveNotification.kt +83 -0
  8. package/android/src/main/java/com/streamvideo/reactnative/keepalive/StreamCallKeepAliveHeadlessService.kt +149 -0
  9. package/android/src/main/java/com/streamvideo/reactnative/screenshare/ScreenAudioCapture.kt +111 -0
  10. package/dist/commonjs/components/Call/CallControls/ScreenShareToggleButton.js +3 -2
  11. package/dist/commonjs/components/Call/CallControls/ScreenShareToggleButton.js.map +1 -1
  12. package/dist/commonjs/hooks/index.js +11 -0
  13. package/dist/commonjs/hooks/index.js.map +1 -1
  14. package/dist/commonjs/hooks/push/index.js +0 -2
  15. package/dist/commonjs/hooks/push/index.js.map +1 -1
  16. package/dist/commonjs/hooks/push/useCallingExpWithCallingStateEffect.js +144 -0
  17. package/dist/commonjs/hooks/push/useCallingExpWithCallingStateEffect.js.map +1 -0
  18. package/dist/commonjs/hooks/push/useIosVoipPushEventsSetupEffect.js +18 -31
  19. package/dist/commonjs/hooks/push/useIosVoipPushEventsSetupEffect.js.map +1 -1
  20. package/dist/commonjs/hooks/useAndroidKeepCallAliveEffect.js +64 -97
  21. package/dist/commonjs/hooks/useAndroidKeepCallAliveEffect.js.map +1 -1
  22. package/dist/commonjs/hooks/useScreenShareAudioMixing.js +126 -0
  23. package/dist/commonjs/hooks/useScreenShareAudioMixing.js.map +1 -0
  24. package/dist/commonjs/hooks/useScreenShareButton.js +57 -3
  25. package/dist/commonjs/hooks/useScreenShareButton.js.map +1 -1
  26. package/dist/commonjs/index.js +1 -0
  27. package/dist/commonjs/index.js.map +1 -1
  28. package/dist/commonjs/modules/ScreenShareAudioManager.js +54 -0
  29. package/dist/commonjs/modules/ScreenShareAudioManager.js.map +1 -0
  30. package/dist/commonjs/modules/call-manager/CallManager.js +26 -0
  31. package/dist/commonjs/modules/call-manager/CallManager.js.map +1 -1
  32. package/dist/commonjs/providers/StreamCall/index.js +16 -6
  33. package/dist/commonjs/providers/StreamCall/index.js.map +1 -1
  34. package/dist/commonjs/utils/StreamVideoRN/index.js +35 -21
  35. package/dist/commonjs/utils/StreamVideoRN/index.js.map +1 -1
  36. package/dist/commonjs/utils/internal/callingx/audioSessionPromise.js +68 -0
  37. package/dist/commonjs/utils/internal/callingx/audioSessionPromise.js.map +1 -0
  38. package/dist/commonjs/utils/internal/callingx/callingx.js +150 -0
  39. package/dist/commonjs/utils/internal/callingx/callingx.js.map +1 -0
  40. package/dist/commonjs/utils/internal/registerSDKGlobals.js +53 -3
  41. package/dist/commonjs/utils/internal/registerSDKGlobals.js.map +1 -1
  42. package/dist/commonjs/utils/keepCallAliveHeadlessTask.js +48 -0
  43. package/dist/commonjs/utils/keepCallAliveHeadlessTask.js.map +1 -0
  44. package/dist/commonjs/utils/push/android.js +135 -202
  45. package/dist/commonjs/utils/push/android.js.map +1 -1
  46. package/dist/commonjs/utils/push/internal/ios.js +17 -34
  47. package/dist/commonjs/utils/push/internal/ios.js.map +1 -1
  48. package/dist/commonjs/utils/push/internal/rxSubjects.js +1 -45
  49. package/dist/commonjs/utils/push/internal/rxSubjects.js.map +1 -1
  50. package/dist/commonjs/utils/push/internal/utils.js +71 -53
  51. package/dist/commonjs/utils/push/internal/utils.js.map +1 -1
  52. package/dist/commonjs/utils/push/ios.js.map +1 -1
  53. package/dist/commonjs/utils/push/libs/callingx.js +75 -0
  54. package/dist/commonjs/utils/push/libs/callingx.js.map +1 -0
  55. package/dist/commonjs/utils/push/libs/index.js +8 -19
  56. package/dist/commonjs/utils/push/libs/index.js.map +1 -1
  57. package/dist/commonjs/utils/push/libs/notifee/index.js +0 -19
  58. package/dist/commonjs/utils/push/libs/notifee/index.js.map +1 -1
  59. package/dist/commonjs/utils/push/setupCallingExpEvents.js +105 -0
  60. package/dist/commonjs/utils/push/setupCallingExpEvents.js.map +1 -0
  61. package/dist/commonjs/utils/push/setupIosVoipPushEvents.js +7 -6
  62. package/dist/commonjs/utils/push/setupIosVoipPushEvents.js.map +1 -1
  63. package/dist/commonjs/version.js +1 -1
  64. package/dist/commonjs/version.js.map +1 -1
  65. package/dist/module/components/Call/CallControls/ScreenShareToggleButton.js +3 -2
  66. package/dist/module/components/Call/CallControls/ScreenShareToggleButton.js.map +1 -1
  67. package/dist/module/hooks/index.js +1 -0
  68. package/dist/module/hooks/index.js.map +1 -1
  69. package/dist/module/hooks/push/index.js +0 -2
  70. package/dist/module/hooks/push/index.js.map +1 -1
  71. package/dist/module/hooks/push/useCallingExpWithCallingStateEffect.js +137 -0
  72. package/dist/module/hooks/push/useCallingExpWithCallingStateEffect.js.map +1 -0
  73. package/dist/module/hooks/push/useIosVoipPushEventsSetupEffect.js +18 -31
  74. package/dist/module/hooks/push/useIosVoipPushEventsSetupEffect.js.map +1 -1
  75. package/dist/module/hooks/useAndroidKeepCallAliveEffect.js +66 -99
  76. package/dist/module/hooks/useAndroidKeepCallAliveEffect.js.map +1 -1
  77. package/dist/module/hooks/useScreenShareAudioMixing.js +119 -0
  78. package/dist/module/hooks/useScreenShareAudioMixing.js.map +1 -0
  79. package/dist/module/hooks/useScreenShareButton.js +57 -3
  80. package/dist/module/hooks/useScreenShareButton.js.map +1 -1
  81. package/dist/module/index.js +1 -0
  82. package/dist/module/index.js.map +1 -1
  83. package/dist/module/modules/ScreenShareAudioManager.js +47 -0
  84. package/dist/module/modules/ScreenShareAudioManager.js.map +1 -0
  85. package/dist/module/modules/call-manager/CallManager.js +26 -0
  86. package/dist/module/modules/call-manager/CallManager.js.map +1 -1
  87. package/dist/module/providers/StreamCall/index.js +16 -6
  88. package/dist/module/providers/StreamCall/index.js.map +1 -1
  89. package/dist/module/utils/StreamVideoRN/index.js +35 -21
  90. package/dist/module/utils/StreamVideoRN/index.js.map +1 -1
  91. package/dist/module/utils/internal/callingx/audioSessionPromise.js +61 -0
  92. package/dist/module/utils/internal/callingx/audioSessionPromise.js.map +1 -0
  93. package/dist/module/utils/internal/callingx/callingx.js +140 -0
  94. package/dist/module/utils/internal/callingx/callingx.js.map +1 -0
  95. package/dist/module/utils/internal/registerSDKGlobals.js +53 -3
  96. package/dist/module/utils/internal/registerSDKGlobals.js.map +1 -1
  97. package/dist/module/utils/keepCallAliveHeadlessTask.js +42 -0
  98. package/dist/module/utils/keepCallAliveHeadlessTask.js.map +1 -0
  99. package/dist/module/utils/push/android.js +137 -204
  100. package/dist/module/utils/push/android.js.map +1 -1
  101. package/dist/module/utils/push/internal/ios.js +17 -34
  102. package/dist/module/utils/push/internal/ios.js.map +1 -1
  103. package/dist/module/utils/push/internal/rxSubjects.js +0 -44
  104. package/dist/module/utils/push/internal/rxSubjects.js.map +1 -1
  105. package/dist/module/utils/push/internal/utils.js +67 -50
  106. package/dist/module/utils/push/internal/utils.js.map +1 -1
  107. package/dist/module/utils/push/ios.js.map +1 -1
  108. package/dist/module/utils/push/libs/callingx.js +67 -0
  109. package/dist/module/utils/push/libs/callingx.js.map +1 -0
  110. package/dist/module/utils/push/libs/index.js +1 -2
  111. package/dist/module/utils/push/libs/index.js.map +1 -1
  112. package/dist/module/utils/push/libs/notifee/index.js +0 -18
  113. package/dist/module/utils/push/libs/notifee/index.js.map +1 -1
  114. package/dist/module/utils/push/setupCallingExpEvents.js +99 -0
  115. package/dist/module/utils/push/setupCallingExpEvents.js.map +1 -0
  116. package/dist/module/utils/push/setupIosVoipPushEvents.js +7 -6
  117. package/dist/module/utils/push/setupIosVoipPushEvents.js.map +1 -1
  118. package/dist/module/version.js +1 -1
  119. package/dist/module/version.js.map +1 -1
  120. package/dist/typescript/components/Call/CallControls/ScreenShareToggleButton.d.ts +6 -1
  121. package/dist/typescript/components/Call/CallControls/ScreenShareToggleButton.d.ts.map +1 -1
  122. package/dist/typescript/hooks/index.d.ts +1 -0
  123. package/dist/typescript/hooks/index.d.ts.map +1 -1
  124. package/dist/typescript/hooks/push/index.d.ts.map +1 -1
  125. package/dist/typescript/hooks/push/useCallingExpWithCallingStateEffect.d.ts +5 -0
  126. package/dist/typescript/hooks/push/useCallingExpWithCallingStateEffect.d.ts.map +1 -0
  127. package/dist/typescript/hooks/push/useIosVoipPushEventsSetupEffect.d.ts.map +1 -1
  128. package/dist/typescript/hooks/useAndroidKeepCallAliveEffect.d.ts.map +1 -1
  129. package/dist/typescript/hooks/useScreenShareAudioMixing.d.ts +14 -0
  130. package/dist/typescript/hooks/useScreenShareAudioMixing.d.ts.map +1 -0
  131. package/dist/typescript/hooks/useScreenShareButton.d.ts +39 -2
  132. package/dist/typescript/hooks/useScreenShareButton.d.ts.map +1 -1
  133. package/dist/typescript/index.d.ts +1 -0
  134. package/dist/typescript/index.d.ts.map +1 -1
  135. package/dist/typescript/modules/ScreenShareAudioManager.d.ts +28 -0
  136. package/dist/typescript/modules/ScreenShareAudioManager.d.ts.map +1 -0
  137. package/dist/typescript/modules/call-manager/CallManager.d.ts +5 -0
  138. package/dist/typescript/modules/call-manager/CallManager.d.ts.map +1 -1
  139. package/dist/typescript/providers/StreamCall/index.d.ts.map +1 -1
  140. package/dist/typescript/utils/StreamVideoRN/index.d.ts +22 -2
  141. package/dist/typescript/utils/StreamVideoRN/index.d.ts.map +1 -1
  142. package/dist/typescript/utils/StreamVideoRN/types.d.ts +59 -25
  143. package/dist/typescript/utils/StreamVideoRN/types.d.ts.map +1 -1
  144. package/dist/typescript/utils/internal/callingx/audioSessionPromise.d.ts +16 -0
  145. package/dist/typescript/utils/internal/callingx/audioSessionPromise.d.ts.map +1 -0
  146. package/dist/typescript/utils/internal/callingx/callingx.d.ts +18 -0
  147. package/dist/typescript/utils/internal/callingx/callingx.d.ts.map +1 -0
  148. package/dist/typescript/utils/internal/registerSDKGlobals.d.ts.map +1 -1
  149. package/dist/typescript/utils/keepCallAliveHeadlessTask.d.ts +10 -0
  150. package/dist/typescript/utils/keepCallAliveHeadlessTask.d.ts.map +1 -0
  151. package/dist/typescript/utils/push/android.d.ts +1 -2
  152. package/dist/typescript/utils/push/android.d.ts.map +1 -1
  153. package/dist/typescript/utils/push/internal/ios.d.ts.map +1 -1
  154. package/dist/typescript/utils/push/internal/rxSubjects.d.ts +0 -33
  155. package/dist/typescript/utils/push/internal/rxSubjects.d.ts.map +1 -1
  156. package/dist/typescript/utils/push/internal/utils.d.ts +14 -8
  157. package/dist/typescript/utils/push/internal/utils.d.ts.map +1 -1
  158. package/dist/typescript/utils/push/ios.d.ts +1 -2
  159. package/dist/typescript/utils/push/ios.d.ts.map +1 -1
  160. package/dist/typescript/utils/push/libs/callingx.d.ts +9 -0
  161. package/dist/typescript/utils/push/libs/callingx.d.ts.map +1 -0
  162. package/dist/typescript/utils/push/libs/firebaseMessaging/index.d.ts +16 -2
  163. package/dist/typescript/utils/push/libs/firebaseMessaging/index.d.ts.map +1 -1
  164. package/dist/typescript/utils/push/libs/index.d.ts +1 -2
  165. package/dist/typescript/utils/push/libs/index.d.ts.map +1 -1
  166. package/dist/typescript/utils/push/libs/notifee/index.d.ts +0 -1
  167. package/dist/typescript/utils/push/libs/notifee/index.d.ts.map +1 -1
  168. package/dist/typescript/utils/push/setupCallingExpEvents.d.ts +8 -0
  169. package/dist/typescript/utils/push/setupCallingExpEvents.d.ts.map +1 -0
  170. package/dist/typescript/utils/push/setupIosVoipPushEvents.d.ts.map +1 -1
  171. package/dist/typescript/version.d.ts +1 -1
  172. package/dist/typescript/version.d.ts.map +1 -1
  173. package/expo-config-plugin/dist/withAndroidManifest.js +1 -33
  174. package/expo-config-plugin/dist/withAndroidPermissions.js +2 -7
  175. package/expo-config-plugin/dist/withAppDelegate.js +19 -197
  176. package/expo-config-plugin/dist/withMainActivity.js +1 -1
  177. package/expo-config-plugin/dist/withiOSInfoPlist.js +2 -3
  178. package/ios/StreamInCallManager.m +2 -0
  179. package/ios/StreamInCallManager.swift +19 -7
  180. package/ios/StreamVideoReactNative.h +7 -4
  181. package/ios/StreamVideoReactNative.m +282 -86
  182. package/package.json +13 -18
  183. package/src/components/Call/CallControls/ScreenShareToggleButton.tsx +11 -1
  184. package/src/hooks/index.ts +1 -0
  185. package/src/hooks/push/index.ts +0 -2
  186. package/src/hooks/push/useCallingExpWithCallingStateEffect.ts +189 -0
  187. package/src/hooks/push/useIosVoipPushEventsSetupEffect.ts +21 -34
  188. package/src/hooks/useAndroidKeepCallAliveEffect.ts +94 -120
  189. package/src/hooks/useScreenShareAudioMixing.ts +130 -0
  190. package/src/hooks/useScreenShareButton.ts +87 -2
  191. package/src/index.ts +1 -0
  192. package/src/modules/ScreenShareAudioManager.ts +49 -0
  193. package/src/modules/call-manager/CallManager.ts +36 -0
  194. package/src/modules/call-manager/native-module.d.ts +7 -0
  195. package/src/providers/StreamCall/index.tsx +17 -6
  196. package/src/utils/StreamVideoRN/index.ts +42 -30
  197. package/src/utils/StreamVideoRN/types.ts +61 -25
  198. package/src/utils/internal/callingx/audioSessionPromise.ts +65 -0
  199. package/src/utils/internal/callingx/callingx.ts +194 -0
  200. package/src/utils/internal/registerSDKGlobals.ts +52 -4
  201. package/src/utils/keepCallAliveHeadlessTask.ts +54 -0
  202. package/src/utils/push/android.ts +198 -311
  203. package/src/utils/push/internal/ios.ts +28 -44
  204. package/src/utils/push/internal/rxSubjects.ts +0 -61
  205. package/src/utils/push/internal/utils.ts +108 -64
  206. package/src/utils/push/ios.ts +1 -6
  207. package/src/utils/push/libs/callingx.ts +89 -0
  208. package/src/utils/push/libs/index.ts +1 -2
  209. package/src/utils/push/libs/notifee/index.ts +0 -27
  210. package/src/utils/push/setupCallingExpEvents.ts +135 -0
  211. package/src/utils/push/setupIosVoipPushEvents.ts +11 -7
  212. package/src/version.ts +1 -1
  213. package/android/src/main/java/com/streamvideo/reactnative/util/CallAliveServiceChecker.kt +0 -95
  214. package/dist/commonjs/hooks/push/useIosCallkeepWithCallingStateEffect.js +0 -160
  215. package/dist/commonjs/hooks/push/useIosCallkeepWithCallingStateEffect.js.map +0 -1
  216. package/dist/commonjs/hooks/push/useProcessPushCallEffect.js +0 -67
  217. package/dist/commonjs/hooks/push/useProcessPushCallEffect.js.map +0 -1
  218. package/dist/commonjs/utils/push/libs/callkeep.js +0 -17
  219. package/dist/commonjs/utils/push/libs/callkeep.js.map +0 -1
  220. package/dist/commonjs/utils/push/libs/voipPushNotification.js +0 -17
  221. package/dist/commonjs/utils/push/libs/voipPushNotification.js.map +0 -1
  222. package/dist/commonjs/utils/push/setupIosCallKeepEvents.js +0 -205
  223. package/dist/commonjs/utils/push/setupIosCallKeepEvents.js.map +0 -1
  224. package/dist/module/hooks/push/useIosCallkeepWithCallingStateEffect.js +0 -153
  225. package/dist/module/hooks/push/useIosCallkeepWithCallingStateEffect.js.map +0 -1
  226. package/dist/module/hooks/push/useProcessPushCallEffect.js +0 -60
  227. package/dist/module/hooks/push/useProcessPushCallEffect.js.map +0 -1
  228. package/dist/module/utils/push/libs/callkeep.js +0 -11
  229. package/dist/module/utils/push/libs/callkeep.js.map +0 -1
  230. package/dist/module/utils/push/libs/voipPushNotification.js +0 -11
  231. package/dist/module/utils/push/libs/voipPushNotification.js.map +0 -1
  232. package/dist/module/utils/push/setupIosCallKeepEvents.js +0 -199
  233. package/dist/module/utils/push/setupIosCallKeepEvents.js.map +0 -1
  234. package/dist/typescript/hooks/push/useIosCallkeepWithCallingStateEffect.d.ts +0 -5
  235. package/dist/typescript/hooks/push/useIosCallkeepWithCallingStateEffect.d.ts.map +0 -1
  236. package/dist/typescript/hooks/push/useProcessPushCallEffect.d.ts +0 -8
  237. package/dist/typescript/hooks/push/useProcessPushCallEffect.d.ts.map +0 -1
  238. package/dist/typescript/utils/push/libs/callkeep.d.ts +0 -3
  239. package/dist/typescript/utils/push/libs/callkeep.d.ts.map +0 -1
  240. package/dist/typescript/utils/push/libs/voipPushNotification.d.ts +0 -3
  241. package/dist/typescript/utils/push/libs/voipPushNotification.d.ts.map +0 -1
  242. package/dist/typescript/utils/push/setupIosCallKeepEvents.d.ts +0 -6
  243. package/dist/typescript/utils/push/setupIosCallKeepEvents.d.ts.map +0 -1
  244. package/src/hooks/push/useIosCallkeepWithCallingStateEffect.ts +0 -235
  245. package/src/hooks/push/useProcessPushCallEffect.ts +0 -108
  246. package/src/utils/push/libs/callkeep.ts +0 -16
  247. package/src/utils/push/libs/voipPushNotification.ts +0 -17
  248. package/src/utils/push/setupIosCallKeepEvents.ts +0 -252
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [1.31.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-native-sdk-1.30.5...@stream-io/video-react-native-sdk-1.31.0) (2026-03-31)
6
+
7
+ ### Dependency Updates
8
+
9
+ - `@stream-io/noise-cancellation-react-native` updated to version `0.6.0`
10
+ - `@stream-io/video-filters-react-native` updated to version `0.11.0`
11
+
12
+ ### Features
13
+
14
+ - screen share audio ([#2157](https://github.com/GetStream/stream-video-js/issues/2157)) ([ba3b9d8](https://github.com/GetStream/stream-video-js/commit/ba3b9d8c2168d7c1cd66050524a5dc0a0f7e3e6e))
15
+
5
16
  ## [1.30.5](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-native-sdk-1.30.4...@stream-io/video-react-native-sdk-1.30.5) (2026-03-27)
6
17
 
7
18
  ### Dependency Updates
@@ -2,6 +2,13 @@
2
2
  package="com.streamvideo.reactnative">
3
3
 
4
4
  <uses-permission android:name="android.permission.INTERNET" />
5
- <uses-permission android:name="android.permission.DEVICE_POWER" />
6
5
  <uses-permission android:name="android.permission.WAKE_LOCK" />
6
+
7
+ <application>
8
+ <service
9
+ android:name="com.streamvideo.reactnative.keepalive.StreamCallKeepAliveHeadlessService"
10
+ android:exported="false"
11
+ android:stopWithTask="true"
12
+ android:foregroundServiceType="mediaPlayback|camera|microphone" />
13
+ </application>
7
14
  </manifest>
@@ -1,2 +1,13 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+
3
+ <uses-permission android:name="android.permission.INTERNET" />
4
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
5
+
6
+ <application>
7
+ <service
8
+ android:name="com.streamvideo.reactnative.keepalive.StreamCallKeepAliveHeadlessService"
9
+ android:exported="false"
10
+ android:stopWithTask="true"
11
+ android:foregroundServiceType="mediaPlayback|camera|microphone" />
12
+ </application>
2
13
  </manifest>
@@ -1,5 +1,6 @@
1
1
  package com.streamvideo.reactnative
2
2
 
3
+ import android.app.Activity
3
4
  import android.content.BroadcastReceiver
4
5
  import android.content.Context
5
6
  import android.content.Intent
@@ -9,12 +10,14 @@ import android.graphics.Bitmap
9
10
  import android.media.AudioAttributes
10
11
  import android.media.AudioFormat
11
12
  import android.media.AudioTrack
13
+ import android.media.projection.MediaProjectionManager
12
14
  import android.net.Uri
13
15
  import android.os.BatteryManager
14
16
  import android.os.Build
15
17
  import android.os.PowerManager
16
18
  import android.util.Base64
17
19
  import android.util.Log
20
+ import androidx.core.content.ContextCompat
18
21
  import com.facebook.react.bridge.Arguments
19
22
  import com.facebook.react.bridge.Promise
20
23
  import com.facebook.react.bridge.ReactApplicationContext
@@ -23,8 +26,10 @@ import com.facebook.react.bridge.ReactMethod
23
26
  import com.facebook.react.bridge.WritableMap
24
27
  import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
25
28
  import com.oney.WebRTCModule.WebRTCModule
29
+ import com.oney.WebRTCModule.WebRTCModuleOptions
30
+ import com.streamvideo.reactnative.screenshare.ScreenAudioCapture
31
+ import com.streamvideo.reactnative.keepalive.StreamCallKeepAliveHeadlessService
26
32
  import com.streamvideo.reactnative.util.CallAlivePermissionsHelper
27
- import com.streamvideo.reactnative.util.CallAliveServiceChecker
28
33
  import com.streamvideo.reactnative.util.PiPHelper
29
34
  import com.streamvideo.reactnative.util.RingtoneUtil
30
35
  import com.streamvideo.reactnative.util.YuvFrame
@@ -52,6 +57,9 @@ class StreamVideoReactNativeModule(reactContext: ReactApplicationContext) :
52
57
  private var busyToneAudioTrack: AudioTrack? = null
53
58
  private var busyToneJob: Job? = null
54
59
 
60
+ // Screen share audio mixing
61
+ private var screenAudioCapture: ScreenAudioCapture? = null
62
+
55
63
  private var thermalStatusListener: PowerManager.OnThermalStatusChangedListener? = null
56
64
 
57
65
  private var batteryChargingStateReceiver = object : BroadcastReceiver() {
@@ -115,11 +123,47 @@ class StreamVideoReactNativeModule(reactContext: ReactApplicationContext) :
115
123
  promise.resolve(false)
116
124
  return
117
125
  }
118
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
119
- val isForegroundServiceDeclared = CallAliveServiceChecker.isForegroundServiceDeclared(reactApplicationContext)
120
- promise.resolve(isForegroundServiceDeclared)
121
- } else {
126
+ // Service is declared in the SDK's own AndroidManifest and merged by default.
127
+ // Permissions are expected to be provided by the app (or via Expo config plugin).
128
+ promise.resolve(true)
129
+ }
130
+
131
+
132
+ @ReactMethod
133
+ fun startKeepCallAliveService(
134
+ callCid: String,
135
+ channelId: String,
136
+ channelName: String,
137
+ title: String,
138
+ body: String,
139
+ smallIconName: String?,
140
+ promise: Promise
141
+ ) {
142
+ try {
143
+ val intent = StreamCallKeepAliveHeadlessService.buildStartIntent(
144
+ reactApplicationContext,
145
+ callCid,
146
+ channelId,
147
+ channelName,
148
+ title,
149
+ body,
150
+ smallIconName
151
+ )
152
+ ContextCompat.startForegroundService(reactApplicationContext, intent)
122
153
  promise.resolve(true)
154
+ } catch (e: Exception) {
155
+ promise.reject(NAME, "Failed to start keep call alive foreground service", e)
156
+ }
157
+ }
158
+
159
+ @ReactMethod
160
+ fun stopKeepCallAliveService(promise: Promise) {
161
+ try {
162
+ val intent = StreamCallKeepAliveHeadlessService.buildStopIntent(reactApplicationContext)
163
+ val stopped = reactApplicationContext.stopService(intent)
164
+ promise.resolve(stopped)
165
+ } catch (e: Exception) {
166
+ promise.reject(NAME, "Failed to stop keep call alive foreground service", e)
123
167
  }
124
168
  }
125
169
 
@@ -148,6 +192,7 @@ class StreamVideoReactNativeModule(reactContext: ReactApplicationContext) :
148
192
  reactApplicationContext.unregisterReceiver(batteryChargingStateReceiver)
149
193
  stopThermalStatusUpdates()
150
194
  stopBusyToneInternal() // Clean up busy tone on invalidate
195
+ stopScreenShareAudioMixingInternal() // Clean up screen share audio on invalidate
151
196
  super.invalidate()
152
197
  }
153
198
 
@@ -484,6 +529,83 @@ class StreamVideoReactNativeModule(reactContext: ReactApplicationContext) :
484
529
  return ShortArray(totalSamples)
485
530
  }
486
531
 
532
+ @ReactMethod
533
+ fun startScreenShareAudioMixing(promise: Promise) {
534
+ try {
535
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
536
+ promise.reject("API_LEVEL", "Screen audio capture requires Android 10 (API 29)+")
537
+ return
538
+ }
539
+
540
+ if (screenAudioCapture != null) {
541
+ Log.w(NAME, "Screen share audio mixing is already active")
542
+ promise.resolve(null)
543
+ return
544
+ }
545
+
546
+ val module = reactApplicationContext.getNativeModule(WebRTCModule::class.java)!!
547
+
548
+ // Get the MediaProjection permission result Intent from WebRTC
549
+ val permissionIntent = module.userMediaImpl?.mediaProjectionPermissionResultData
550
+ if (permissionIntent == null) {
551
+ promise.reject("NO_PROJECTION", "No MediaProjection permission available. Start screen sharing first.")
552
+ return
553
+ }
554
+
555
+ // Create a MediaProjection for audio capture
556
+ val mediaProjectionManager = reactApplicationContext.getSystemService(
557
+ Context.MEDIA_PROJECTION_SERVICE
558
+ ) as MediaProjectionManager
559
+ val mediaProjection = mediaProjectionManager.getMediaProjection(
560
+ Activity.RESULT_OK, permissionIntent
561
+ )
562
+ if (mediaProjection == null) {
563
+ promise.reject("PROJECTION_ERROR", "Failed to create MediaProjection for audio capture")
564
+ return
565
+ }
566
+
567
+ screenAudioCapture = ScreenAudioCapture(mediaProjection).also { it.start() }
568
+
569
+ // Register the screen audio bytes provider so the AudioBufferCallback
570
+ // in WebRTCModule mixes screen audio into the mic buffer.
571
+ WebRTCModuleOptions.getInstance().screenAudioBytesProvider =
572
+ WebRTCModuleOptions.ScreenAudioBytesProvider { bytesRequested ->
573
+ screenAudioCapture?.getScreenAudioBytes(bytesRequested)
574
+ }
575
+
576
+ Log.d(NAME, "Screen share audio mixing started")
577
+ promise.resolve(null)
578
+ } catch (e: Exception) {
579
+ Log.e(NAME, "Error starting screen share audio mixing: ${e.message}")
580
+ promise.reject("ERROR", e.message, e)
581
+ }
582
+ }
583
+
584
+ @ReactMethod
585
+ fun stopScreenShareAudioMixing(promise: Promise) {
586
+ try {
587
+ stopScreenShareAudioMixingInternal()
588
+ promise.resolve(null)
589
+ } catch (e: Exception) {
590
+ Log.e(NAME, "Error stopping screen share audio mixing: ${e.message}")
591
+ promise.reject("ERROR", e.message, e)
592
+ }
593
+ }
594
+
595
+ private fun stopScreenShareAudioMixingInternal() {
596
+ try {
597
+ // Clear the provider so the AudioBufferCallback stops mixing
598
+ WebRTCModuleOptions.getInstance().screenAudioBytesProvider = null
599
+
600
+ screenAudioCapture?.stop()
601
+ screenAudioCapture = null
602
+
603
+ Log.d(NAME, "Screen share audio mixing stopped")
604
+ } catch (e: Exception) {
605
+ Log.e(NAME, "Error in stopScreenShareAudioMixingInternal: ${e.message}")
606
+ }
607
+ }
608
+
487
609
  companion object {
488
610
  private const val NAME = "StreamVideoReactNative"
489
611
  private const val SAMPLE_RATE = 22050
@@ -55,14 +55,78 @@ object WebRtcAudioUtils {
55
55
  * what might be the root cause.
56
56
  */
57
57
  fun logAudioState(tag: String, reactContext: ReactContext) {
58
+ Log.d(tag, getAudioStateLog(reactContext))
59
+ }
60
+
61
+ /**
62
+ * Returns a string containing information about the current audio state.
63
+ * Similar to logAudioState but returns the information instead of logging it.
64
+ */
65
+ fun getAudioStateLog(reactContext: ReactContext): String {
66
+ val audioManager = reactContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
67
+ val sb = StringBuilder()
68
+
69
+ // Volume control stream
58
70
  reactContext.currentActivity?.let {
59
- Log.d(tag, "volumeControlStream: " + streamTypeToString(it.volumeControlStream))
71
+ sb.appendLine("volumeControlStream: ${streamTypeToString(it.volumeControlStream)}")
60
72
  }
61
- val audioManager = reactContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
62
- logDeviceInfo(tag)
63
- logAudioStateBasic(tag, reactContext, audioManager)
64
- logAudioStateVolume(tag, audioManager)
65
- logAudioDeviceInfo(tag, audioManager)
73
+
74
+ // Device info
75
+ sb.appendLine("Android SDK: ${Build.VERSION.SDK_INT}, Release: ${Build.VERSION.RELEASE}, Brand: ${Build.BRAND}, Device: ${Build.DEVICE}, Id: ${Build.ID}, Hardware: ${Build.HARDWARE}, Manufacturer: ${Build.MANUFACTURER}, Model: ${Build.MODEL}, Product: ${Build.PRODUCT}")
76
+
77
+ // Basic audio state
78
+ sb.appendLine("Audio State: audio mode: ${modeToString(audioManager.mode)}, has mic: ${hasMicrophone(reactContext)}, mic muted: ${audioManager.isMicrophoneMute}, music active: ${audioManager.isMusicActive}, speakerphone: ${audioManager.isSpeakerphoneOn}, BT SCO: ${audioManager.isBluetoothScoOn}")
79
+
80
+ // Volume info
81
+ val fixedVolume = audioManager.isVolumeFixed
82
+ sb.appendLine(" fixed volume=$fixedVolume")
83
+ if (!fixedVolume) {
84
+ val streams = intArrayOf(
85
+ AudioManager.STREAM_VOICE_CALL,
86
+ AudioManager.STREAM_MUSIC,
87
+ AudioManager.STREAM_RING,
88
+ AudioManager.STREAM_ALARM,
89
+ AudioManager.STREAM_NOTIFICATION,
90
+ AudioManager.STREAM_SYSTEM
91
+ )
92
+ for (stream in streams) {
93
+ val info = StringBuilder()
94
+ info.append(" ${streamTypeToString(stream)}: ")
95
+ info.append("volume=${audioManager.getStreamVolume(stream)}")
96
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
97
+ info.append(", min=${audioManager.getStreamMinVolume(stream)}")
98
+ }
99
+ info.append(", max=${audioManager.getStreamMaxVolume(stream)}")
100
+ info.append(", muted=${audioManager.isStreamMute(stream)}")
101
+ sb.appendLine(info.toString())
102
+ }
103
+ }
104
+
105
+ // Audio devices
106
+ val inputDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
107
+ val outputDevices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
108
+ val devices = inputDevices + outputDevices
109
+ if (devices.isNotEmpty()) {
110
+ sb.appendLine("Audio Devices:")
111
+ for (device in devices) {
112
+ val info = StringBuilder()
113
+ info.append(" ${deviceTypeToString(device.type)}")
114
+ info.append(if (device.isSource) "(in): " else "(out): ")
115
+ if (device.channelCounts.isNotEmpty()) {
116
+ info.append("channels=${device.channelCounts.contentToString()}, ")
117
+ }
118
+ if (device.encodings.isNotEmpty()) {
119
+ info.append("encodings=${device.encodings.contentToString()}, ")
120
+ }
121
+ if (device.sampleRates.isNotEmpty()) {
122
+ info.append("sample rates=${device.sampleRates.contentToString()}, ")
123
+ }
124
+ info.append("id=${device.id}")
125
+ sb.appendLine(info.toString())
126
+ }
127
+ }
128
+
129
+ return sb.toString()
66
130
  }
67
131
 
68
132
  /** Converts AudioDeviceInfo types to local string representation. */
@@ -168,10 +168,12 @@ class StreamInCallManagerModule(reactContext: ReactApplicationContext) :
168
168
 
169
169
  @ReactMethod
170
170
  fun logAudioState() {
171
- WebRtcAudioUtils.logAudioState(
172
- TAG,
173
- reactApplicationContext,
174
- )
171
+ Log.d(TAG, getAudioStateLog())
172
+ }
173
+
174
+ @ReactMethod(isBlockingSynchronousMethod = true)
175
+ fun getAudioStateLog(): String {
176
+ return WebRtcAudioUtils.getAudioStateLog(reactApplicationContext)
175
177
  }
176
178
 
177
179
  @Suppress("unused")
@@ -0,0 +1,83 @@
1
+ package com.streamvideo.reactnative.keepalive
2
+
3
+ import android.app.Notification
4
+ import android.app.NotificationChannel
5
+ import android.app.NotificationManager
6
+ import android.app.PendingIntent
7
+ import android.content.Context
8
+ import android.content.Intent
9
+ import android.content.pm.PackageManager
10
+ import android.os.Build
11
+ import androidx.core.app.NotificationCompat
12
+
13
+ internal object KeepAliveNotification {
14
+ private const val DEFAULT_CHANNEL_DESCRIPTION = "Stream call keep-alive"
15
+
16
+ fun ensureChannel(
17
+ context: Context,
18
+ channelId: String,
19
+ channelName: String
20
+ ) {
21
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
22
+ val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
23
+ val existing = manager.getNotificationChannel(channelId)
24
+ if (existing != null) return
25
+
26
+ val channel = NotificationChannel(
27
+ channelId,
28
+ channelName,
29
+ NotificationManager.IMPORTANCE_LOW
30
+ ).apply {
31
+ description = DEFAULT_CHANNEL_DESCRIPTION
32
+ setShowBadge(false)
33
+ }
34
+ manager.createNotificationChannel(channel)
35
+ }
36
+
37
+ fun buildOngoingNotification(
38
+ context: Context,
39
+ channelId: String,
40
+ title: String,
41
+ body: String,
42
+ smallIconName: String?
43
+ ): Notification {
44
+ val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
45
+ val pendingIntentFlags =
46
+ PendingIntent.FLAG_UPDATE_CURRENT or
47
+ PendingIntent.FLAG_IMMUTABLE
48
+ val contentIntent = if (launchIntent != null) {
49
+ PendingIntent.getActivity(context, 0, launchIntent, pendingIntentFlags)
50
+ } else {
51
+ // Fallback: empty intent to avoid crash if launch activity is missing for some reason
52
+ PendingIntent.getActivity(context, 0, Intent(), pendingIntentFlags)
53
+ }
54
+
55
+ val iconResId = resolveSmallIconResId(context, smallIconName)
56
+ return NotificationCompat.Builder(context, channelId)
57
+ .setContentTitle(title)
58
+ .setContentText(body)
59
+ .setOngoing(true)
60
+ .setOnlyAlertOnce(true)
61
+ .setCategory(NotificationCompat.CATEGORY_CALL)
62
+ .setContentIntent(contentIntent)
63
+ .setSmallIcon(iconResId)
64
+ .build()
65
+ }
66
+
67
+ private fun resolveSmallIconResId(context: Context, smallIconName: String?): Int {
68
+ val resources = context.resources
69
+ val packageName = context.packageName
70
+ if (!smallIconName.isNullOrBlank()) {
71
+ val id = resources.getIdentifier(smallIconName, "drawable", packageName)
72
+ if (id != 0) return id
73
+ }
74
+ // Default to the app icon
75
+ return try {
76
+ val appInfo = context.packageManager.getApplicationInfo(packageName, 0)
77
+ appInfo.icon
78
+ } catch (_: PackageManager.NameNotFoundException) {
79
+ android.R.drawable.ic_dialog_info
80
+ }
81
+ }
82
+ }
83
+
@@ -0,0 +1,149 @@
1
+ package com.streamvideo.reactnative.keepalive
2
+
3
+ import android.Manifest
4
+ import android.content.Intent
5
+ import android.content.pm.PackageManager
6
+ import android.content.pm.ServiceInfo
7
+ import android.os.Build
8
+ import android.util.Log
9
+ import androidx.annotation.RequiresApi
10
+ import androidx.core.app.ServiceCompat
11
+ import androidx.core.content.ContextCompat
12
+ import com.facebook.react.HeadlessJsTaskService
13
+ import com.facebook.react.bridge.Arguments
14
+ import com.facebook.react.jstasks.HeadlessJsTaskConfig
15
+
16
+ /**
17
+ * Foreground service that runs a React Native HeadlessJS task to keep a call alive.
18
+ *
19
+ */
20
+ class StreamCallKeepAliveHeadlessService : HeadlessJsTaskService() {
21
+
22
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
23
+ val safeIntent = intent ?: Intent()
24
+ val channelId = safeIntent.getStringExtra(EXTRA_CHANNEL_ID) ?: DEFAULT_CHANNEL_ID
25
+ val channelName = safeIntent.getStringExtra(EXTRA_CHANNEL_NAME) ?: DEFAULT_CHANNEL_NAME
26
+ val title = safeIntent.getStringExtra(EXTRA_TITLE) ?: DEFAULT_TITLE
27
+ val body = safeIntent.getStringExtra(EXTRA_BODY) ?: DEFAULT_BODY
28
+ val smallIconName = safeIntent.getStringExtra(EXTRA_SMALL_ICON_NAME)
29
+
30
+ KeepAliveNotification.ensureChannel(this, channelId, channelName)
31
+ val notification = KeepAliveNotification.buildOngoingNotification(
32
+ context = this,
33
+ channelId = channelId,
34
+ title = title,
35
+ body = body,
36
+ smallIconName = smallIconName
37
+ )
38
+
39
+ startForegroundCompat(notification)
40
+
41
+ // Ensure HeadlessJS task is started
42
+ return super.onStartCommand(safeIntent, flags, startId)
43
+ }
44
+
45
+ override fun getTaskConfig(intent: Intent?): HeadlessJsTaskConfig? {
46
+ val callCid = intent?.getStringExtra(EXTRA_CALL_CID) ?: return null
47
+ val data = Arguments.createMap().apply {
48
+ putString("callCid", callCid)
49
+ }
50
+ // We intentionally allow long-running work (the JS task can return a never-resolving Promise).
51
+ return HeadlessJsTaskConfig(
52
+ TASK_NAME,
53
+ data,
54
+ 0, // timeout (0 = no timeout)
55
+ true // allowedInForeground
56
+ )
57
+ }
58
+
59
+ override fun onDestroy() {
60
+ super.onDestroy()
61
+ stopForeground(STOP_FOREGROUND_REMOVE)
62
+ }
63
+
64
+ @RequiresApi(Build.VERSION_CODES.R)
65
+ private fun computeForegroundServiceTypes(): Int {
66
+ var types = ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
67
+
68
+ val hasCameraPermission =
69
+ ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
70
+ if (hasCameraPermission) {
71
+ types = types or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
72
+ }
73
+
74
+ val hasMicrophonePermission =
75
+ ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
76
+ if (hasMicrophonePermission) {
77
+ types = types or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
78
+ }
79
+
80
+ return types
81
+ }
82
+
83
+ private fun startForegroundCompat(notification: android.app.Notification) {
84
+ try {
85
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
86
+ val types =
87
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) computeForegroundServiceTypes()
88
+ else ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
89
+ startForeground(NOTIFICATION_ID, notification, types)
90
+ } else {
91
+ startForeground(NOTIFICATION_ID, notification)
92
+ }
93
+ } catch (e: Exception) {
94
+ // Avoid crashing the app if the system rejects starting a foreground service (e.g.
95
+ // background start restrictions, invalid notification/channel, or permission issues).
96
+ Log.e(
97
+ TAG,
98
+ "startForegroundCompat: Failed to start foreground service: ${e.message}",
99
+ e
100
+ )
101
+ }
102
+ }
103
+
104
+ companion object {
105
+ private const val TAG = "StreamCallKeepAliveHeadlessService"
106
+
107
+ const val TASK_NAME = "StreamVideoKeepCallAlive"
108
+
109
+ const val EXTRA_CALL_CID = "callCid"
110
+ const val EXTRA_CHANNEL_ID = "channelId"
111
+ const val EXTRA_CHANNEL_NAME = "channelName"
112
+ const val EXTRA_TITLE = "title"
113
+ const val EXTRA_BODY = "body"
114
+ const val EXTRA_SMALL_ICON_NAME = "smallIconName"
115
+
116
+ private const val NOTIFICATION_ID = 6061
117
+
118
+ private const val DEFAULT_CHANNEL_ID = "stream_call_foreground_service"
119
+ private const val DEFAULT_CHANNEL_NAME = "Call in progress"
120
+ private const val DEFAULT_TITLE = "Call in progress"
121
+ private const val DEFAULT_BODY = "Tap to return to the call"
122
+
123
+ fun buildStartIntent(
124
+ context: android.content.Context,
125
+ callCid: String,
126
+ channelId: String,
127
+ channelName: String,
128
+ title: String,
129
+ body: String,
130
+ smallIconName: String?
131
+ ): Intent {
132
+ return Intent(context, StreamCallKeepAliveHeadlessService::class.java).apply {
133
+ putExtra(EXTRA_CALL_CID, callCid)
134
+ putExtra(EXTRA_CHANNEL_ID, channelId)
135
+ putExtra(EXTRA_CHANNEL_NAME, channelName)
136
+ putExtra(EXTRA_TITLE, title)
137
+ putExtra(EXTRA_BODY, body)
138
+ if (!smallIconName.isNullOrBlank()) {
139
+ putExtra(EXTRA_SMALL_ICON_NAME, smallIconName)
140
+ }
141
+ }
142
+ }
143
+
144
+ fun buildStopIntent(context: android.content.Context): Intent {
145
+ return Intent(context, StreamCallKeepAliveHeadlessService::class.java)
146
+ }
147
+ }
148
+ }
149
+
@@ -0,0 +1,111 @@
1
+ package com.streamvideo.reactnative.screenshare
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.media.AudioAttributes
5
+ import android.media.AudioFormat
6
+ import android.media.AudioPlaybackCaptureConfiguration
7
+ import android.media.AudioRecord
8
+ import android.media.projection.MediaProjection
9
+ import android.os.Build
10
+ import android.util.Log
11
+ import androidx.annotation.RequiresApi
12
+ import java.nio.ByteBuffer
13
+
14
+ /**
15
+ * Captures system media audio using [AudioPlaybackCaptureConfiguration].
16
+ *
17
+ * Uses the given [MediaProjection] to set up an [AudioRecord] that captures
18
+ * audio from media playback, games, and other apps (USAGE_MEDIA, USAGE_GAME,
19
+ * USAGE_UNKNOWN) but not notifications, alarms, or system sounds.
20
+ *
21
+ * Audio is captured in a pull-based manner via [getScreenAudioBytes], which
22
+ * reads exactly the requested number of bytes using [AudioRecord.READ_BLOCKING].
23
+ * This is designed to be called from the WebRTC audio processing thread.
24
+ *
25
+ * Format: 48kHz, mono, PCM 16-bit (matching WebRTC's audio pipeline).
26
+ *
27
+ * Requires Android 10 (API 29+).
28
+ */
29
+ @RequiresApi(Build.VERSION_CODES.Q)
30
+ class ScreenAudioCapture(private val mediaProjection: MediaProjection) {
31
+
32
+ private var audioRecord: AudioRecord? = null
33
+ private var screenAudioBuffer: ByteBuffer? = null
34
+
35
+ companion object {
36
+ private const val TAG = "ScreenAudioCapture"
37
+ const val SAMPLE_RATE = 48000
38
+ private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO
39
+ private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT
40
+ }
41
+
42
+ @SuppressLint("MissingPermission")
43
+ fun start() {
44
+ val playbackConfig = AudioPlaybackCaptureConfiguration.Builder(mediaProjection)
45
+ .addMatchingUsage(AudioAttributes.USAGE_MEDIA)
46
+ .addMatchingUsage(AudioAttributes.USAGE_GAME)
47
+ .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN)
48
+ .build()
49
+
50
+ val audioFormat = AudioFormat.Builder()
51
+ .setSampleRate(SAMPLE_RATE)
52
+ .setChannelMask(CHANNEL_CONFIG)
53
+ .setEncoding(AUDIO_FORMAT)
54
+ .build()
55
+
56
+ audioRecord = AudioRecord.Builder()
57
+ .setAudioFormat(audioFormat)
58
+ .setAudioPlaybackCaptureConfig(playbackConfig)
59
+ .build()
60
+
61
+ if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
62
+ Log.e(TAG, "AudioRecord failed to initialize")
63
+ audioRecord?.release()
64
+ audioRecord = null
65
+ return
66
+ }
67
+
68
+ audioRecord?.startRecording()
69
+ Log.d(TAG, "Screen audio capture started")
70
+ }
71
+
72
+ /**
73
+ * Pull-based read: returns a [ByteBuffer] containing exactly [bytesRequested] bytes
74
+ * of captured screen audio.
75
+ *
76
+ * Called from the WebRTC audio processing thread. Uses [AudioRecord.READ_BLOCKING]
77
+ * so it will block until the requested bytes are available.
78
+ *
79
+ * @return A [ByteBuffer] with screen audio data, or `null` if capture is not active.
80
+ */
81
+ fun getScreenAudioBytes(bytesRequested: Int): ByteBuffer? {
82
+ val record = audioRecord ?: return null
83
+ if (bytesRequested <= 0) return null
84
+
85
+ val buffer = screenAudioBuffer?.takeIf { it.capacity() >= bytesRequested }
86
+ ?: ByteBuffer.allocateDirect(bytesRequested).also { screenAudioBuffer = it }
87
+
88
+ buffer.clear()
89
+ buffer.limit(bytesRequested)
90
+
91
+ val bytesRead = record.read(buffer, bytesRequested, AudioRecord.READ_BLOCKING)
92
+ if (bytesRead > 0) {
93
+ buffer.position(0)
94
+ buffer.limit(bytesRead)
95
+ return buffer
96
+ }
97
+ return null
98
+ }
99
+
100
+ fun stop() {
101
+ try {
102
+ audioRecord?.stop()
103
+ } catch (e: Exception) {
104
+ Log.w(TAG, "Error stopping AudioRecord: ${e.message}")
105
+ }
106
+ audioRecord?.release()
107
+ audioRecord = null
108
+ screenAudioBuffer = null
109
+ Log.d(TAG, "Screen audio capture stopped")
110
+ }
111
+ }