@stream-io/video-react-native-sdk 1.44.0 → 1.45.1
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.
- package/CHANGELOG.md +35 -3
- package/android/src/main/java/com/streamvideo/reactnative/audio/AudioDeviceManager.kt +110 -58
- package/android/src/main/java/com/streamvideo/reactnative/audio/CommunicationModeKeepAlive.kt +247 -0
- package/android/src/main/java/com/streamvideo/reactnative/callmanager/StreamInCallManagerModule.kt +16 -5
- package/android/src/main/java/com/streamvideo/reactnative/keepalive/StreamCallKeepAliveHeadlessService.kt +112 -26
- package/android/src/main/java/com/streamvideo/reactnative/util/CallAlivePermissionsHelper.kt +68 -37
- package/dist/commonjs/hooks/push/useCallingExpWithCallingStateEffect.js +12 -6
- package/dist/commonjs/hooks/push/useCallingExpWithCallingStateEffect.js.map +1 -1
- package/dist/commonjs/hooks/useAndroidKeepCallAliveEffect.js +38 -11
- package/dist/commonjs/hooks/useAndroidKeepCallAliveEffect.js.map +1 -1
- package/dist/commonjs/hooks/useLoopbackRecording.js +10 -127
- package/dist/commonjs/hooks/useLoopbackRecording.js.map +1 -1
- package/dist/commonjs/utils/StreamVideoRN/index.js +25 -0
- package/dist/commonjs/utils/StreamVideoRN/index.js.map +1 -1
- package/dist/commonjs/utils/internal/callingx/callingx.js +38 -56
- package/dist/commonjs/utils/internal/callingx/callingx.js.map +1 -1
- package/dist/commonjs/utils/keepCallAliveHeadlessTask.js +43 -4
- package/dist/commonjs/utils/keepCallAliveHeadlessTask.js.map +1 -1
- package/dist/commonjs/utils/push/android.js +7 -167
- package/dist/commonjs/utils/push/android.js.map +1 -1
- package/dist/commonjs/utils/push/internal/android.js +204 -0
- package/dist/commonjs/utils/push/internal/android.js.map +1 -0
- package/dist/commonjs/utils/push/libs/firebaseMessaging/index.js +9 -1
- package/dist/commonjs/utils/push/libs/firebaseMessaging/index.js.map +1 -1
- package/dist/commonjs/utils/push/libs/firebaseMessaging/lib.js +1 -1
- package/dist/commonjs/utils/push/libs/firebaseMessaging/lib.js.map +1 -1
- package/dist/commonjs/utils/push/setupAndroidPushEvents.js +25 -0
- package/dist/commonjs/utils/push/setupAndroidPushEvents.js.map +1 -0
- package/dist/commonjs/utils/push/setupCallingExpEvents.js +6 -2
- package/dist/commonjs/utils/push/setupCallingExpEvents.js.map +1 -1
- package/dist/commonjs/utils/push/setupIosVoipPushEvents.js +6 -7
- package/dist/commonjs/utils/push/setupIosVoipPushEvents.js.map +1 -1
- package/dist/commonjs/utils/push/utils.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/module/hooks/push/useCallingExpWithCallingStateEffect.js +12 -6
- package/dist/module/hooks/push/useCallingExpWithCallingStateEffect.js.map +1 -1
- package/dist/module/hooks/useAndroidKeepCallAliveEffect.js +39 -12
- package/dist/module/hooks/useAndroidKeepCallAliveEffect.js.map +1 -1
- package/dist/module/hooks/useLoopbackRecording.js +10 -127
- package/dist/module/hooks/useLoopbackRecording.js.map +1 -1
- package/dist/module/utils/StreamVideoRN/index.js +25 -0
- package/dist/module/utils/StreamVideoRN/index.js.map +1 -1
- package/dist/module/utils/internal/callingx/callingx.js +38 -56
- package/dist/module/utils/internal/callingx/callingx.js.map +1 -1
- package/dist/module/utils/keepCallAliveHeadlessTask.js +41 -3
- package/dist/module/utils/keepCallAliveHeadlessTask.js.map +1 -1
- package/dist/module/utils/push/android.js +9 -169
- package/dist/module/utils/push/android.js.map +1 -1
- package/dist/module/utils/push/internal/android.js +200 -0
- package/dist/module/utils/push/internal/android.js.map +1 -0
- package/dist/module/utils/push/libs/firebaseMessaging/index.js +9 -1
- package/dist/module/utils/push/libs/firebaseMessaging/index.js.map +1 -1
- package/dist/module/utils/push/libs/firebaseMessaging/lib.js +1 -1
- package/dist/module/utils/push/libs/firebaseMessaging/lib.js.map +1 -1
- package/dist/module/utils/push/setupAndroidPushEvents.js +21 -0
- package/dist/module/utils/push/setupAndroidPushEvents.js.map +1 -0
- package/dist/module/utils/push/setupCallingExpEvents.js +6 -2
- package/dist/module/utils/push/setupCallingExpEvents.js.map +1 -1
- package/dist/module/utils/push/setupIosVoipPushEvents.js +6 -7
- package/dist/module/utils/push/setupIosVoipPushEvents.js.map +1 -1
- package/dist/module/utils/push/utils.js.map +1 -1
- package/dist/module/version.js +1 -1
- package/dist/typescript/hooks/push/useCallingExpWithCallingStateEffect.d.ts.map +1 -1
- package/dist/typescript/hooks/useAndroidKeepCallAliveEffect.d.ts.map +1 -1
- package/dist/typescript/hooks/useLoopbackRecording.d.ts +12 -69
- package/dist/typescript/hooks/useLoopbackRecording.d.ts.map +1 -1
- package/dist/typescript/utils/StreamVideoRN/index.d.ts +13 -0
- package/dist/typescript/utils/StreamVideoRN/index.d.ts.map +1 -1
- package/dist/typescript/utils/StreamVideoRN/types.d.ts +1 -2
- package/dist/typescript/utils/StreamVideoRN/types.d.ts.map +1 -1
- package/dist/typescript/utils/internal/callingx/callingx.d.ts.map +1 -1
- package/dist/typescript/utils/keepCallAliveHeadlessTask.d.ts +9 -0
- package/dist/typescript/utils/keepCallAliveHeadlessTask.d.ts.map +1 -1
- package/dist/typescript/utils/push/android.d.ts +3 -4
- package/dist/typescript/utils/push/android.d.ts.map +1 -1
- package/dist/typescript/utils/push/internal/android.d.ts +8 -0
- package/dist/typescript/utils/push/internal/android.d.ts.map +1 -0
- package/dist/typescript/utils/push/libs/firebaseMessaging/index.d.ts +6 -4
- package/dist/typescript/utils/push/libs/firebaseMessaging/index.d.ts.map +1 -1
- package/dist/typescript/utils/push/libs/firebaseMessaging/lib.d.ts +2 -2
- package/dist/typescript/utils/push/libs/firebaseMessaging/lib.d.ts.map +1 -1
- package/dist/typescript/utils/push/setupAndroidPushEvents.d.ts +3 -0
- package/dist/typescript/utils/push/setupAndroidPushEvents.d.ts.map +1 -0
- package/dist/typescript/utils/push/setupCallingExpEvents.d.ts.map +1 -1
- package/dist/typescript/utils/push/setupIosVoipPushEvents.d.ts.map +1 -1
- package/dist/typescript/utils/push/utils.d.ts +2 -2
- package/dist/typescript/utils/push/utils.d.ts.map +1 -1
- package/dist/typescript/version.d.ts +1 -1
- package/expo-config-plugin/dist/withAndroidPermissions.js +1 -1
- package/ios/TracksRecorder/RecorderVideoSink.swift +92 -5
- package/package.json +4 -4
- package/src/hooks/push/useCallingExpWithCallingStateEffect.ts +25 -11
- package/src/hooks/useAndroidKeepCallAliveEffect.ts +59 -21
- package/src/hooks/useLoopbackRecording.ts +36 -248
- package/src/modules/call-manager/native-module.d.ts +14 -0
- package/src/utils/StreamVideoRN/index.ts +29 -0
- package/src/utils/StreamVideoRN/types.ts +1 -2
- package/src/utils/internal/callingx/callingx.ts +49 -68
- package/src/utils/keepCallAliveHeadlessTask.ts +45 -3
- package/src/utils/push/android.ts +12 -237
- package/src/utils/push/internal/android.ts +273 -0
- package/src/utils/push/libs/firebaseMessaging/index.ts +16 -5
- package/src/utils/push/libs/firebaseMessaging/lib.ts +3 -3
- package/src/utils/push/setupAndroidPushEvents.ts +25 -0
- package/src/utils/push/setupCallingExpEvents.ts +6 -3
- package/src/utils/push/setupIosVoipPushEvents.ts +8 -7
- package/src/utils/push/utils.ts +2 -4
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
|
|
4
4
|
|
|
5
|
+
## [1.45.1](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-native-sdk-1.45.0...@stream-io/video-react-native-sdk-1.45.1) (2026-09-04)
|
|
6
|
+
|
|
7
|
+
### Dependency Updates
|
|
8
|
+
|
|
9
|
+
- `@stream-io/react-native-callingx` updated to version `0.11.3`
|
|
10
|
+
- `@stream-io/video-client` updated to version `1.59.1`
|
|
11
|
+
- **client:** isolate coordinator event listener errors from the dispatch loop ([#2404](https://github.com/GetStream/stream-video-js/issues/2404)) ([4ab49de](https://github.com/GetStream/stream-video-js/commit/4ab49decc4bbc70b0fc35d6b92f2bd18922a105c)), closes [GetStream/stream-chat-js#1850](https://github.com/GetStream/stream-chat-js/issues/1850)
|
|
12
|
+
- **client:** keep passive participants out of the spotlight ([#2405](https://github.com/GetStream/stream-video-js/issues/2405)) ([0713650](https://github.com/GetStream/stream-video-js/commit/0713650fa90caa8a82ce0bee75acf4dbaaad33e9))
|
|
13
|
+
- `@stream-io/video-react-bindings` updated to version `1.20.2`
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- **callingx:** don't tear down concurrent calls on stop service ([#2394](https://github.com/GetStream/stream-video-js/issues/2394)) ([d6e9deb](https://github.com/GetStream/stream-video-js/commit/d6e9debbe98ae91cc349f010eb92f76b35a3f932))
|
|
18
|
+
- **rn:** auto-leave previous callingx calls on new join ([#2402](https://github.com/GetStream/stream-video-js/issues/2402)) ([5a3dfdd](https://github.com/GetStream/stream-video-js/commit/5a3dfddd8ddbfacf6e8ea109656be82da91a3436))
|
|
19
|
+
- **rn:** wrong expo plugin Android call-alive foreground service detection ([#2398](https://github.com/GetStream/stream-video-js/issues/2398)) ([eac10b5](https://github.com/GetStream/stream-video-js/commit/eac10b5a390f6949b25535d2b4c4cf9bc3d5f065))
|
|
20
|
+
- workaround for android non-telecom path MODE_IN_COMMUNICATION reset edge-case ([#2360](https://github.com/GetStream/stream-video-js/issues/2360)) ([65a1fb7](https://github.com/GetStream/stream-video-js/commit/65a1fb766461f95a3b70fe221e9ace0caad6c64b))
|
|
21
|
+
|
|
22
|
+
## [1.45.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-native-sdk-1.44.0...@stream-io/video-react-native-sdk-1.45.0) (2026-08-28)
|
|
23
|
+
|
|
24
|
+
### Dependency Updates
|
|
25
|
+
|
|
26
|
+
- `@stream-io/react-native-callingx` updated to version `0.11.0`
|
|
27
|
+
- `@stream-io/video-client` updated to version `1.59.0`
|
|
28
|
+
- `@stream-io/video-react-bindings` updated to version `1.20.1`
|
|
29
|
+
|
|
30
|
+
### Features
|
|
31
|
+
|
|
32
|
+
- handle ring notification internally ([#2380](https://github.com/GetStream/stream-video-js/issues/2380)) ([0259217](https://github.com/GetStream/stream-video-js/commit/0259217ea2df613483bee5918ca2f93a68b6c3b9))
|
|
33
|
+
- **react-sdk:** pre-call testing ([#2370](https://github.com/GetStream/stream-video-js/issues/2370)) ([3f4b62a](https://github.com/GetStream/stream-video-js/commit/3f4b62ab379d9d4203bc64b5d5e3658fba389efd))
|
|
34
|
+
|
|
35
|
+
### Bug Fixes
|
|
36
|
+
|
|
37
|
+
- do not allow empty android call display names ([#2392](https://github.com/GetStream/stream-video-js/issues/2392)) ([2ef3065](https://github.com/GetStream/stream-video-js/commit/2ef3065038fc293d0e2c96d2cc26feaf317e7b2d))
|
|
38
|
+
- fixed ringing calls mute issue ([#2385](https://github.com/GetStream/stream-video-js/issues/2385)) ([b3bcdad](https://github.com/GetStream/stream-video-js/commit/b3bcdadc9f9f4eedf7dabde101b96e44863dcc64))
|
|
39
|
+
|
|
5
40
|
## [1.44.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-native-sdk-1.43.0...@stream-io/video-react-native-sdk-1.44.0) (2026-08-18)
|
|
6
41
|
|
|
7
42
|
### Dependency Updates
|
|
@@ -10,9 +45,6 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/s
|
|
|
10
45
|
- `@stream-io/react-native-callingx` updated to version `0.10.0`
|
|
11
46
|
- `@stream-io/video-filters-react-native` updated to version `0.16.0`
|
|
12
47
|
- `@stream-io/video-client` updated to version `1.58.0`
|
|
13
|
-
- **client:** E2EE framing-version detection and event renames ([#2366](https://github.com/GetStream/stream-video-js/issues/2366)) ([050aebf](https://github.com/GetStream/stream-video-js/commit/050aebf6efea84089bee6ded5e62ba02ecc22716))
|
|
14
|
-
- **client:** granular E2EE key removal ([#2368](https://github.com/GetStream/stream-video-js/issues/2368)) ([8f52b9a](https://github.com/GetStream/stream-video-js/commit/8f52b9a6ff9b87382dcd5ebd7414388153be8a13)), closes [#2366](https://github.com/GetStream/stream-video-js/issues/2366)
|
|
15
|
-
- **client:** only deselect the speaker device on a real disconnect ([#2375](https://github.com/GetStream/stream-video-js/issues/2375)) ([d7f780d](https://github.com/GetStream/stream-video-js/commit/d7f780d0924471adfe9ba85accf42e889b01e20b))
|
|
16
48
|
- `@stream-io/video-react-bindings` updated to version `1.20.0`
|
|
17
49
|
|
|
18
50
|
### Features
|
|
@@ -107,6 +107,30 @@ class AudioDeviceManager(
|
|
|
107
107
|
*/
|
|
108
108
|
var telecomManagedMode: Boolean = false
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Opt-out for the Android 11+ communication-mode keep-alive. Sticky developer preference —
|
|
112
|
+
* intentionally NOT reset in [stop], unlike the per-call fields above.
|
|
113
|
+
*/
|
|
114
|
+
var disableCommunicationModeWorkaround: Boolean = false
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* True once [stop]/[close] have torn routing down, until the next [start].
|
|
118
|
+
*
|
|
119
|
+
* [updateAudioDeviceState] is enqueued from off-thread sources (Bluetooth receivers,
|
|
120
|
+
* audio-focus and device callbacks), so a task can still land after teardown. Re-routing
|
|
121
|
+
* then would re-assert MODE_IN_COMMUNICATION via AudioManagerUtil, undoing the
|
|
122
|
+
* MODE_NORMAL restore in [stop]. Audio-thread confined.
|
|
123
|
+
*/
|
|
124
|
+
private var routingStopped = false
|
|
125
|
+
|
|
126
|
+
/** Keeps MODE_IN_COMMUNICATION owned for the whole Communicator call on Android 11+. */
|
|
127
|
+
private val communicationModeKeepAlive: CommunicationModeKeepAlive =
|
|
128
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
129
|
+
SilentPlaybackKeepAlive(mReactContext)
|
|
130
|
+
} else {
|
|
131
|
+
NoCommunicationModeKeepAlive
|
|
132
|
+
}
|
|
133
|
+
|
|
110
134
|
val bluetoothManager = BluetoothManager(mReactContext, this)
|
|
111
135
|
|
|
112
136
|
private val proximityManager by lazy { ProximityManager(mReactContext, this) }
|
|
@@ -130,53 +154,64 @@ class AudioDeviceManager(
|
|
|
130
154
|
audioFocusUtil.setup(callAudioRole, mReactContext)
|
|
131
155
|
}
|
|
132
156
|
|
|
157
|
+
/** Runs inline; callers must already be on the audio thread (see [runInAudioThread]). */
|
|
133
158
|
fun start(activity: Activity) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (!telecomManagedMode) {
|
|
142
|
-
// Telecom owns routing/focus; only run our own routing when not Telecom-managed.
|
|
143
|
-
bluetoothManager.start()
|
|
144
|
-
mAudioManager.registerAudioDeviceCallback(this, null)
|
|
145
|
-
updateAudioDeviceState()
|
|
146
|
-
}
|
|
147
|
-
proximityManager.start()
|
|
148
|
-
} else {
|
|
149
|
-
activity.volumeControlStream = AudioManager.USE_DEFAULT_STREAM_TYPE
|
|
150
|
-
}
|
|
159
|
+
routingStopped = false
|
|
160
|
+
setup()
|
|
161
|
+
selectedAudioDeviceEndpoint = null
|
|
162
|
+
if (callAudioRole == CallAudioRole.Communicator) {
|
|
163
|
+
// Audio routing is manually controlled by the SDK in communication media mode
|
|
164
|
+
// and local microphone can be published
|
|
165
|
+
activity.volumeControlStream = AudioManager.STREAM_VOICE_CALL
|
|
151
166
|
if (!telecomManagedMode) {
|
|
152
|
-
|
|
167
|
+
// Telecom owns routing/focus; only run our own routing when not Telecom-managed.
|
|
168
|
+
bluetoothManager.start()
|
|
169
|
+
mAudioManager.registerAudioDeviceCallback(this, null)
|
|
170
|
+
updateAudioDeviceState()
|
|
153
171
|
}
|
|
172
|
+
proximityManager.start()
|
|
173
|
+
} else {
|
|
174
|
+
activity.volumeControlStream = AudioManager.USE_DEFAULT_STREAM_TYPE
|
|
175
|
+
}
|
|
176
|
+
if (!telecomManagedMode) {
|
|
177
|
+
audioFocusUtil.requestFocus(callAudioRole, mReactContext)
|
|
178
|
+
}
|
|
179
|
+
// Started last, after focus/routing, so the silent track uses the correct route.
|
|
180
|
+
if (callAudioRole == CallAudioRole.Communicator &&
|
|
181
|
+
!telecomManagedMode &&
|
|
182
|
+
!disableCommunicationModeWorkaround
|
|
183
|
+
) {
|
|
184
|
+
communicationModeKeepAlive.start()
|
|
154
185
|
}
|
|
155
186
|
}
|
|
156
187
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (Build.VERSION.SDK_INT >= 31) {
|
|
163
|
-
mAudioManager.clearCommunicationDevice()
|
|
164
|
-
} else {
|
|
165
|
-
mAudioManager.setSpeakerphoneOn(false)
|
|
166
|
-
}
|
|
167
|
-
bluetoothManager.stop()
|
|
168
|
-
}
|
|
169
|
-
callAudioRole = CallAudioRole.Communicator
|
|
170
|
-
enableStereo = false
|
|
171
|
-
defaultAudioDevice = AudioDeviceEndpoint.TYPE_SPEAKER
|
|
172
|
-
proximityManager.stop()
|
|
173
|
-
}
|
|
174
|
-
activity.volumeControlStream = AudioManager.USE_DEFAULT_STREAM_TYPE
|
|
188
|
+
/** Runs inline; callers must already be on the audio thread (see [runInAudioThread]). */
|
|
189
|
+
fun stop(activity: Activity?) {
|
|
190
|
+
routingStopped = true
|
|
191
|
+
communicationModeKeepAlive.stop()
|
|
192
|
+
if (callAudioRole == CallAudioRole.Communicator) {
|
|
175
193
|
if (!telecomManagedMode) {
|
|
176
|
-
|
|
194
|
+
// Only tear down what we set up ourselves; Telecom owns its own teardown.
|
|
195
|
+
if (Build.VERSION.SDK_INT >= 31) {
|
|
196
|
+
mAudioManager.clearCommunicationDevice()
|
|
197
|
+
} else {
|
|
198
|
+
mAudioManager.setSpeakerphoneOn(false)
|
|
199
|
+
}
|
|
200
|
+
bluetoothManager.stop()
|
|
201
|
+
// Restore the mode set in setup(); it was previously left at
|
|
202
|
+
// MODE_IN_COMMUNICATION, holding the device in in-call routing.
|
|
203
|
+
mAudioManager.mode = AudioManager.MODE_NORMAL
|
|
177
204
|
}
|
|
178
|
-
|
|
205
|
+
callAudioRole = CallAudioRole.Communicator
|
|
206
|
+
enableStereo = false
|
|
207
|
+
defaultAudioDevice = AudioDeviceEndpoint.TYPE_SPEAKER
|
|
208
|
+
proximityManager.stop()
|
|
179
209
|
}
|
|
210
|
+
activity?.volumeControlStream = AudioManager.USE_DEFAULT_STREAM_TYPE
|
|
211
|
+
if (!telecomManagedMode) {
|
|
212
|
+
audioFocusUtil.abandonFocus()
|
|
213
|
+
}
|
|
214
|
+
telecomManagedMode = false
|
|
180
215
|
}
|
|
181
216
|
|
|
182
217
|
fun setMicrophoneMute(enable: Boolean) {
|
|
@@ -228,6 +263,9 @@ class AudioDeviceManager(
|
|
|
228
263
|
}
|
|
229
264
|
|
|
230
265
|
private fun switchDeviceEndpointType(@EndpointType deviceType: Int) {
|
|
266
|
+
// Sole sink for routing changes, and AudioManagerUtil re-asserts MODE_IN_COMMUNICATION
|
|
267
|
+
// here — so late work of any origin must not reach it after teardown.
|
|
268
|
+
if (routingStopped) return
|
|
231
269
|
val newDevice = AudioManagerUtil.switchDeviceEndpointType(
|
|
232
270
|
deviceType,
|
|
233
271
|
mEndpointMaps,
|
|
@@ -237,38 +275,49 @@ class AudioDeviceManager(
|
|
|
237
275
|
this.selectedAudioDeviceEndpoint = newDevice
|
|
238
276
|
}
|
|
239
277
|
|
|
278
|
+
/** Runs inline; callers must already be on the audio thread (see [runInAudioThread]). */
|
|
240
279
|
fun switchDeviceById(
|
|
241
280
|
deviceId: String
|
|
242
281
|
) {
|
|
243
282
|
Log.d(TAG, "switchDeviceById: deviceId = $deviceId")
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
AudioDeviceEndpoint.TYPE_BLUETOOTH
|
|
254
|
-
)
|
|
255
|
-
}
|
|
283
|
+
// Guarded here too: the branch below sets the communication device directly rather
|
|
284
|
+
// than going through switchDeviceEndpointType().
|
|
285
|
+
if (routingStopped) return
|
|
286
|
+
val btDevice = mEndpointMaps.bluetoothEndpoints[deviceId]
|
|
287
|
+
if (btDevice != null) {
|
|
288
|
+
if (Build.VERSION.SDK_INT >= 31) {
|
|
289
|
+
mAudioManager.setCommunicationDevice(btDevice.deviceInfo)
|
|
290
|
+
bluetoothManager.updateDevice()
|
|
291
|
+
this.selectedAudioDeviceEndpoint = btDevice
|
|
256
292
|
} else {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
293
|
+
switchDeviceEndpointType(
|
|
294
|
+
AudioDeviceEndpoint.TYPE_BLUETOOTH
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
val endpoint = nonBluetoothEndpointById(deviceId)
|
|
299
|
+
if (endpoint != null) {
|
|
300
|
+
switchDeviceEndpointType(endpoint.type)
|
|
301
|
+
} else {
|
|
302
|
+
Log.e(TAG, "switchDeviceById: no endpoint found for id $deviceId")
|
|
263
303
|
}
|
|
264
304
|
}
|
|
265
305
|
}
|
|
266
306
|
|
|
267
307
|
override fun close() {
|
|
268
|
-
|
|
269
|
-
|
|
308
|
+
// Unlike start()/stop(), this is called straight from the module, so it must be queued
|
|
309
|
+
// to land after any start/stop already pending on the audio thread.
|
|
310
|
+
runInAudioThread {
|
|
311
|
+
routingStopped = true
|
|
312
|
+
communicationModeKeepAlive.release()
|
|
313
|
+
mAudioManager.unregisterAudioDeviceCallback(this)
|
|
314
|
+
proximityManager.onDestroy()
|
|
315
|
+
}
|
|
270
316
|
}
|
|
271
317
|
|
|
318
|
+
/** Short description of the keep-alive state, for the audio debug log. */
|
|
319
|
+
fun communicationModeKeepAliveState(): String = communicationModeKeepAlive.describeState()
|
|
320
|
+
|
|
272
321
|
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
|
|
273
322
|
if (addedDevices != null) {
|
|
274
323
|
runInAudioThread {
|
|
@@ -423,6 +472,9 @@ class AudioDeviceManager(
|
|
|
423
472
|
*/
|
|
424
473
|
fun updateAudioDeviceState() {
|
|
425
474
|
runInAudioThread {
|
|
475
|
+
// Bail early: on API < 31 bluetoothState survives stop(), so a late pass could
|
|
476
|
+
// still start SCO after the call ended.
|
|
477
|
+
if (routingStopped) return@runInAudioThread
|
|
426
478
|
if (telecomManagedMode) {
|
|
427
479
|
// Telecom is the single source of truth for routing/endpoints in this mode.
|
|
428
480
|
return@runInAudioThread
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
package com.streamvideo.reactnative.audio
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Context.AUDIO_SERVICE
|
|
6
|
+
import android.media.AudioAttributes
|
|
7
|
+
import android.media.AudioFormat
|
|
8
|
+
import android.media.AudioManager
|
|
9
|
+
import android.media.AudioTrack
|
|
10
|
+
import android.os.Build
|
|
11
|
+
import android.util.Log
|
|
12
|
+
import androidx.annotation.RequiresApi
|
|
13
|
+
import com.streamvideo.reactnative.callmanager.StreamInCallManagerModule
|
|
14
|
+
import java.nio.ByteBuffer
|
|
15
|
+
import java.util.concurrent.Executors
|
|
16
|
+
import java.util.concurrent.ScheduledExecutorService
|
|
17
|
+
import java.util.concurrent.TimeUnit
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Holds the audio system in [AudioManager.MODE_IN_COMMUNICATION] for the duration
|
|
21
|
+
* of a communicator-role call.
|
|
22
|
+
*
|
|
23
|
+
* Why this is needed: from Android 11 (API 30) the platform demotes the
|
|
24
|
+
* audio mode back to `MODE_NORMAL` about six seconds after `setMode()` unless the
|
|
25
|
+
* call is actively playing or recording on the voice-communication path.
|
|
26
|
+
* During a call whose microphone is muted and that has no inbound audio yet,
|
|
27
|
+
* neither condition is met, so routing silently falls back to the media path and
|
|
28
|
+
* hardware echo cancellation is switched off.
|
|
29
|
+
* Details: https://issuetracker.google.com/issues/209493718
|
|
30
|
+
*
|
|
31
|
+
* We satisfy the "actively playing" condition by continuously looping an inaudible
|
|
32
|
+
* audo track on the voice-communication stream, which keeps the platform counting us
|
|
33
|
+
* as an active player and therefore keeps the mode in place.
|
|
34
|
+
*/
|
|
35
|
+
interface CommunicationModeKeepAlive {
|
|
36
|
+
/** Begin holding the communication mode. Idempotent. */
|
|
37
|
+
fun start()
|
|
38
|
+
|
|
39
|
+
/** Stop holding the communication mode. Idempotent; the instance can be started again. */
|
|
40
|
+
fun stop()
|
|
41
|
+
|
|
42
|
+
/** Permanently release all resources. The instance must not be used afterwards. */
|
|
43
|
+
fun release()
|
|
44
|
+
|
|
45
|
+
/** Short human-readable state, surfaced in the audio-state debug log. */
|
|
46
|
+
fun describeState(): String
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** No-op variant used on platforms where the mode reset does not occur (Android API < 30). */
|
|
50
|
+
object NoCommunicationModeKeepAlive : CommunicationModeKeepAlive {
|
|
51
|
+
override fun start() {}
|
|
52
|
+
override fun stop() {}
|
|
53
|
+
override fun release() {}
|
|
54
|
+
override fun describeState(): String = "disabled (android platform API < 30)"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Default implementation backed by a silent, looping voice-communication
|
|
59
|
+
* [AudioTrack]. If the silent player cannot be created on a given device, it
|
|
60
|
+
* degrades to a lightweight poller that re-applies the mode whenever the platform
|
|
61
|
+
* has reset it.
|
|
62
|
+
*
|
|
63
|
+
* All lifecycle transitions and every access to [track] are serialized on [gate],
|
|
64
|
+
* so a play/pause can never overlap a release. The poller only ever touches the
|
|
65
|
+
* audio mode (never the track), so it runs lock-free.
|
|
66
|
+
*
|
|
67
|
+
* @suppress
|
|
68
|
+
*/
|
|
69
|
+
@RequiresApi(Build.VERSION_CODES.R)
|
|
70
|
+
internal class SilentPlaybackKeepAlive(
|
|
71
|
+
private val context: Context,
|
|
72
|
+
) : CommunicationModeKeepAlive {
|
|
73
|
+
|
|
74
|
+
private val gate = Any()
|
|
75
|
+
|
|
76
|
+
private var track: AudioTrack? = null
|
|
77
|
+
|
|
78
|
+
/** Whether we currently intend the silent player to be running. */
|
|
79
|
+
private var engaged = false
|
|
80
|
+
|
|
81
|
+
/** Read by the poller (off-gate) to drop ticks that were queued before we stopped. */
|
|
82
|
+
@Volatile
|
|
83
|
+
private var live = false
|
|
84
|
+
|
|
85
|
+
private var modePoller: ScheduledExecutorService? = null
|
|
86
|
+
|
|
87
|
+
override fun start(): Unit = synchronized(gate) {
|
|
88
|
+
live = true
|
|
89
|
+
if (engaged) return
|
|
90
|
+
engaged = true
|
|
91
|
+
|
|
92
|
+
val player = track ?: createSilentTrack()?.also { track = it }
|
|
93
|
+
if (player != null && player.state == AudioTrack.STATE_INITIALIZED && play(player)) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
// No usable silent player on this device — fall back to reactive repair.
|
|
97
|
+
player?.release()
|
|
98
|
+
track = null
|
|
99
|
+
startModePoller()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Starts playback, reporting whether the track actually entered the playing state.
|
|
104
|
+
* `play()` can throw or leave the track stopped even from STATE_INITIALIZED; treating
|
|
105
|
+
* that as success would strand the keep-alive with neither a track nor the poller.
|
|
106
|
+
*/
|
|
107
|
+
private fun play(player: AudioTrack): Boolean =
|
|
108
|
+
try {
|
|
109
|
+
player.play()
|
|
110
|
+
player.playState == AudioTrack.PLAYSTATE_PLAYING
|
|
111
|
+
} catch (e: IllegalStateException) {
|
|
112
|
+
Log.w(TAG, "Silent keep-alive track refused to start.", e)
|
|
113
|
+
false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
override fun stop() = synchronized(gate) {
|
|
117
|
+
live = false
|
|
118
|
+
if (engaged) {
|
|
119
|
+
engaged = false
|
|
120
|
+
track?.takeIf { it.state == AudioTrack.STATE_INITIALIZED }?.pause()
|
|
121
|
+
}
|
|
122
|
+
stopModePoller()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
override fun release() = synchronized(gate) {
|
|
126
|
+
live = false
|
|
127
|
+
engaged = false
|
|
128
|
+
stopModePoller()
|
|
129
|
+
track?.let {
|
|
130
|
+
if (it.state == AudioTrack.STATE_INITIALIZED) it.pause()
|
|
131
|
+
it.release()
|
|
132
|
+
}
|
|
133
|
+
track = null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
override fun describeState(): String = synchronized(gate) {
|
|
137
|
+
val playing = track?.playState == AudioTrack.PLAYSTATE_PLAYING
|
|
138
|
+
"enabled, engaged=$engaged, built=${track != null}, playing=$playing, " +
|
|
139
|
+
"modePoller=${modePoller != null}"
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@SuppressLint("Range")
|
|
143
|
+
private fun createSilentTrack(): AudioTrack? {
|
|
144
|
+
return try {
|
|
145
|
+
val audioManager = context.getSystemService(AUDIO_SERVICE) as AudioManager
|
|
146
|
+
val sampleRate = positiveOrDefault(
|
|
147
|
+
audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)?.toIntOrNull(),
|
|
148
|
+
DEFAULT_SAMPLE_RATE,
|
|
149
|
+
)
|
|
150
|
+
// One buffer worth of frames is enough — we loop it forever.
|
|
151
|
+
val frameCount = positiveOrDefault(
|
|
152
|
+
audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER)
|
|
153
|
+
?.toIntOrNull(),
|
|
154
|
+
sampleRate / 100, // ~10 ms
|
|
155
|
+
)
|
|
156
|
+
val bufferBytes = frameCount * BYTES_PER_FRAME
|
|
157
|
+
// A freshly allocated direct buffer is zero-filled, i.e. pure silence.
|
|
158
|
+
val silence = ByteBuffer.allocateDirect(bufferBytes)
|
|
159
|
+
|
|
160
|
+
val player = AudioTrack.Builder()
|
|
161
|
+
.setAudioAttributes(
|
|
162
|
+
AudioAttributes.Builder()
|
|
163
|
+
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
|
164
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
|
165
|
+
.build(),
|
|
166
|
+
)
|
|
167
|
+
.setAudioFormat(
|
|
168
|
+
AudioFormat.Builder()
|
|
169
|
+
.setEncoding(ENCODING)
|
|
170
|
+
.setSampleRate(sampleRate)
|
|
171
|
+
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
|
|
172
|
+
.build(),
|
|
173
|
+
)
|
|
174
|
+
.setBufferSizeInBytes(bufferBytes)
|
|
175
|
+
.setTransferMode(AudioTrack.MODE_STATIC)
|
|
176
|
+
.setSessionId(AudioManager.AUDIO_SESSION_ID_GENERATE)
|
|
177
|
+
.build()
|
|
178
|
+
|
|
179
|
+
val written = player.write(silence, silence.remaining(), AudioTrack.WRITE_BLOCKING)
|
|
180
|
+
val loopResult = player.setLoopPoints(0, frameCount - 1, -1)
|
|
181
|
+
val ready = player.state == AudioTrack.STATE_INITIALIZED &&
|
|
182
|
+
written >= 0 &&
|
|
183
|
+
loopResult == AudioTrack.SUCCESS
|
|
184
|
+
if (ready) {
|
|
185
|
+
player
|
|
186
|
+
} else {
|
|
187
|
+
Log.w(
|
|
188
|
+
TAG,
|
|
189
|
+
"Silent keep-alive track not usable " +
|
|
190
|
+
"(state=${player.state}, written=$written, loop=$loopResult); discarding.",
|
|
191
|
+
)
|
|
192
|
+
player.release()
|
|
193
|
+
null
|
|
194
|
+
}
|
|
195
|
+
} catch (e: Exception) {
|
|
196
|
+
Log.w(TAG, "Could not create silent keep-alive track.", e)
|
|
197
|
+
null
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private fun startModePoller() {
|
|
202
|
+
if (modePoller != null) return
|
|
203
|
+
modePoller = Executors.newSingleThreadScheduledExecutor().also { poller ->
|
|
204
|
+
poller.scheduleWithFixedDelay(
|
|
205
|
+
::reapplyModeIfReset,
|
|
206
|
+
MODE_POLL_INTERVAL_MS,
|
|
207
|
+
MODE_POLL_INTERVAL_MS,
|
|
208
|
+
TimeUnit.MILLISECONDS,
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private fun reapplyModeIfReset() {
|
|
214
|
+
// Read/write the mode on the shared audio thread so it can't race routing changes.
|
|
215
|
+
AudioDeviceManager.runInAudioThread {
|
|
216
|
+
if (!live) return@runInAudioThread
|
|
217
|
+
val audioManager = context.getSystemService(AUDIO_SERVICE) as AudioManager
|
|
218
|
+
if (audioManager.mode != AudioManager.MODE_IN_COMMUNICATION) {
|
|
219
|
+
Log.d(TAG, "Re-applying MODE_IN_COMMUNICATION after a platform reset.")
|
|
220
|
+
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private fun stopModePoller() {
|
|
226
|
+
modePoller?.shutdownNow()
|
|
227
|
+
modePoller = null
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private fun positiveOrDefault(value: Int?, default: Int): Int =
|
|
231
|
+
if (value != null && value > 0) value else default
|
|
232
|
+
|
|
233
|
+
companion object {
|
|
234
|
+
private const val TAG = StreamInCallManagerModule.TAG
|
|
235
|
+
private const val DEFAULT_SAMPLE_RATE = 16000
|
|
236
|
+
private const val ENCODING = AudioFormat.ENCODING_PCM_16BIT
|
|
237
|
+
|
|
238
|
+
// We always emit mono 16-bit PCM, so a frame is exactly 2 bytes.
|
|
239
|
+
private const val BYTES_PER_FRAME = 2
|
|
240
|
+
|
|
241
|
+
// Fallback poll cadence. Deliberately shorter than AOSP's ~6s reset window: the
|
|
242
|
+
// interval bounds only the worst-case wrong-routing gap (<= interval), while the
|
|
243
|
+
// real setMode/HAL transition rate stays governed by the OS re-arm, so polling
|
|
244
|
+
// faster only costs cheap reads and shortens the recovery latency.
|
|
245
|
+
private const val MODE_POLL_INTERVAL_MS = 2000L
|
|
246
|
+
}
|
|
247
|
+
}
|
package/android/src/main/java/com/streamvideo/reactnative/callmanager/StreamInCallManagerModule.kt
CHANGED
|
@@ -75,6 +75,18 @@ class StreamInCallManagerModule(reactContext: ReactApplicationContext) :
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
@ReactMethod
|
|
79
|
+
fun setDisableCommunicationModeWorkaround(disabled: Boolean) {
|
|
80
|
+
AudioDeviceManager.runInAudioThread {
|
|
81
|
+
if (audioManagerActivated) {
|
|
82
|
+
Log.e(TAG, "setDisableCommunicationModeWorkaround(): AudioManager is already activated and so it cannot be changed")
|
|
83
|
+
return@runInAudioThread
|
|
84
|
+
}
|
|
85
|
+
Log.d(TAG, "setDisableCommunicationModeWorkaround(): $disabled")
|
|
86
|
+
mAudioDeviceManager.disableCommunicationModeWorkaround = disabled
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
78
90
|
@ReactMethod
|
|
79
91
|
fun setDefaultAudioDeviceEndpointType(endpointDeviceTypeName: String) {
|
|
80
92
|
AudioDeviceManager.runInAudioThread {
|
|
@@ -129,10 +141,8 @@ class StreamInCallManagerModule(reactContext: ReactApplicationContext) :
|
|
|
129
141
|
AudioDeviceManager.runInAudioThread {
|
|
130
142
|
if (audioManagerActivated) {
|
|
131
143
|
Log.d(TAG, "stop() mAudioDeviceManager")
|
|
132
|
-
reactApplicationContext.currentActivity
|
|
133
|
-
|
|
134
|
-
audioManagerActivated = false
|
|
135
|
-
}
|
|
144
|
+
mAudioDeviceManager.stop(reactApplicationContext.currentActivity)
|
|
145
|
+
audioManagerActivated = false
|
|
136
146
|
setMicrophoneMute(false)
|
|
137
147
|
setKeepScreenOn(false)
|
|
138
148
|
}
|
|
@@ -185,7 +195,8 @@ class StreamInCallManagerModule(reactContext: ReactApplicationContext) :
|
|
|
185
195
|
|
|
186
196
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
187
197
|
fun getAudioStateLog(): String {
|
|
188
|
-
return WebRtcAudioUtils.getAudioStateLog(reactApplicationContext)
|
|
198
|
+
return WebRtcAudioUtils.getAudioStateLog(reactApplicationContext) +
|
|
199
|
+
"Communication mode keep-alive: ${mAudioDeviceManager.communicationModeKeepAliveState()}\n"
|
|
189
200
|
}
|
|
190
201
|
|
|
191
202
|
@Suppress("unused")
|