@apirtc/expo-apirtc-options-plugin 0.0.3 → 0.0.5
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/README.md +72 -38
- package/build/static/kotlin/BackgroundBlurModule.kt +420 -0
- package/build/static/kotlin/BackgroundBlurPackage.kt +16 -0
- package/build/static/kotlin/BackgroundImageProcessor.kt +433 -0
- package/build/static/kotlin/BlurVideoProcessor.kt +448 -0
- package/build/withAndroidPlugin.d.ts +1 -0
- package/build/withAndroidPlugin.js +72 -16
- package/build/withIosRPKFiles.js +1 -1
- package/build/withPlugin.d.ts +1 -0
- package/build/withPlugin.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,62 +1,96 @@
|
|
|
1
1
|
# expo-apirtc-options-plugin
|
|
2
2
|
|
|
3
3
|
Plugin to add apiRTC features in React Native application using Expo.
|
|
4
|
-
This plugin
|
|
5
|
-
-
|
|
4
|
+
This plugin simplifies the integration of the following features:
|
|
5
|
+
- Screen sharing for Android and iOS
|
|
6
|
+
- Background blur and background image replacement for Android (via ML Kit Selfie Segmentation)
|
|
6
7
|
|
|
7
8
|
## Installation
|
|
8
9
|
|
|
9
10
|
```bash
|
|
10
11
|
npm install @apirtc/expo-apirtc-options-plugin
|
|
11
|
-
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
## Usage in app.json
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Declare the plugin in your `app.json`:
|
|
17
17
|
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"expo": {
|
|
21
|
+
"plugins": [
|
|
22
|
+
["@apirtc/expo-apirtc-options-plugin", {
|
|
23
|
+
"enableMediaProjectionService": true,
|
|
24
|
+
"enableVideoEffects": true,
|
|
25
|
+
"appleTeamId": "YOUR_TEAM_ID"
|
|
26
|
+
}]
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
28
30
|
```
|
|
29
31
|
|
|
30
|
-
###
|
|
32
|
+
### Options
|
|
31
33
|
|
|
32
|
-
| Parameter | Description | Default
|
|
34
|
+
| Parameter | Description | Default |
|
|
33
35
|
| --- | --- | --- |
|
|
34
|
-
| enableMediaProjectionService |
|
|
35
|
-
|
|
|
36
|
+
| `enableMediaProjectionService` | Enable screen sharing on Android (MediaProjection service) | `true` |
|
|
37
|
+
| `enableVideoEffects` | Enable background blur and background image replacement on Android | `true` |
|
|
38
|
+
| `appleTeamId` | Apple Team ID used for the iOS broadcast extension | `"APPLE_TEAM_ID_NOT_SET"` |
|
|
39
|
+
|
|
40
|
+
## What does this plugin do?
|
|
41
|
+
|
|
42
|
+
### Android
|
|
43
|
+
|
|
44
|
+
- Adds required permissions to `AndroidManifest.xml` for screen sharing and camera access
|
|
45
|
+
- Registers `AppLifecyclePackage` to handle app lifecycle events (e.g. releasing streams when the app is killed)
|
|
46
|
+
- When `enableVideoEffects: true`:
|
|
47
|
+
- Copies native Kotlin files (`BackgroundBlurModule`, `BlurVideoProcessor`, `BackgroundImageProcessor`, `BackgroundBlurPackage`) to the Android source tree
|
|
48
|
+
- Registers `BackgroundBlurPackage` in `MainApplication.kt`
|
|
49
|
+
- Adds `com.google.mlkit:segmentation-selfie:16.0.0-beta6` to `app/build.gradle`
|
|
50
|
+
|
|
51
|
+
### iOS
|
|
36
52
|
|
|
37
|
-
|
|
53
|
+
- Adds a broadcast extension for screen sharing
|
|
54
|
+
- Configures the project with all modifications needed for screen sharing
|
|
55
|
+
|
|
56
|
+
## Using video effects from JavaScript (Android only)
|
|
57
|
+
|
|
58
|
+
After enabling `enableVideoEffects: true`, the `BackgroundBlurModule` native module is available:
|
|
38
59
|
|
|
39
60
|
```js
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
plugins: [
|
|
43
|
-
['@apirtc/expo-apirtc-options-plugin', {
|
|
44
|
-
enableMediaProjectionService: false,
|
|
45
|
-
appleTeamId: "YOUR_TEAM_ID"
|
|
46
|
-
}]
|
|
47
|
-
],
|
|
48
|
-
};
|
|
49
|
-
```
|
|
61
|
+
import { NativeModules, Platform } from 'react-native';
|
|
62
|
+
const { BackgroundBlurModule } = NativeModules;
|
|
50
63
|
|
|
51
|
-
|
|
64
|
+
// Enable background blur
|
|
65
|
+
if (Platform.OS === 'android') {
|
|
66
|
+
await BackgroundBlurModule.enableBlur({
|
|
67
|
+
trackId: videoTrack.id, // video track ID from the local stream
|
|
68
|
+
strong: false, // true for stronger blur (3 passes)
|
|
69
|
+
});
|
|
70
|
+
}
|
|
52
71
|
|
|
53
|
-
|
|
72
|
+
// Enable background image replacement
|
|
73
|
+
if (Platform.OS === 'android') {
|
|
74
|
+
await BackgroundBlurModule.enableBackgroundImage({
|
|
75
|
+
trackId: videoTrack.id,
|
|
76
|
+
imageUrl: 'https://example.com/background.jpg',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Disable any active video effect
|
|
81
|
+
if (Platform.OS === 'android') {
|
|
82
|
+
await BackgroundBlurModule.disableBlur();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check if an effect is active
|
|
86
|
+
if (Platform.OS === 'android') {
|
|
87
|
+
const isActive = await BackgroundBlurModule.isBlurEnabled();
|
|
88
|
+
}
|
|
89
|
+
```
|
|
54
90
|
|
|
55
|
-
|
|
56
|
-
- Manage the case where the application is killed by user when a screenShare is enabled :
|
|
57
|
-
A native module AppLifecycleModule is added in your application to manage the case where the user kill the application using swipe.
|
|
58
|
-
This module detect the onHostDestroy event to stop the screenSharing extension and unpublish stream.
|
|
91
|
+
## Notes
|
|
59
92
|
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
93
|
+
- `enableVideoEffects` is Android-only. The option is silently ignored on iOS.
|
|
94
|
+
- ML Kit Selfie Segmentation (`16.0.0-beta6`) is currently in beta — there is no GA release of this library.
|
|
95
|
+
- Background effects are applied via WebRTC's `VideoProcessor` API, so both the local preview and remote peers see the processed video.
|
|
96
|
+
- For persistence across sessions, save the selected effect ID with `AsyncStorage` and re-apply it after `createStreamFromUserMedia()`.
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
package com.reactnativeapirtc
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
6
|
+
import com.facebook.react.bridge.ReactMethod
|
|
7
|
+
import com.facebook.react.bridge.Promise
|
|
8
|
+
import com.facebook.react.bridge.ReadableMap
|
|
9
|
+
import com.oney.WebRTCModule.WebRTCModule
|
|
10
|
+
import org.webrtc.VideoSource
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* React Native module that provides background blur functionality for Android.
|
|
14
|
+
*
|
|
15
|
+
* Architecture:
|
|
16
|
+
* This module finds the WebRTC VideoSource (camera source) via reflection
|
|
17
|
+
* and sets a VideoProcessor on it. The VideoProcessor intercepts every
|
|
18
|
+
* camera frame, applies ML Kit segmentation + RenderScript blur, and
|
|
19
|
+
* forwards the processed frame to the downstream pipeline.
|
|
20
|
+
*
|
|
21
|
+
* Camera → VideoSource → [VideoProcessor] → VideoTrack → RTCView + Encoder
|
|
22
|
+
*
|
|
23
|
+
* This ensures both local preview AND remote peers see the blurred video.
|
|
24
|
+
*
|
|
25
|
+
* Exposed methods to JavaScript:
|
|
26
|
+
* - enableBlur(config): Promise
|
|
27
|
+
* - disableBlur(): Promise
|
|
28
|
+
* - isBlurEnabled(): Promise<boolean>
|
|
29
|
+
*/
|
|
30
|
+
class BackgroundBlurModule(reactContext: ReactApplicationContext) :
|
|
31
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
32
|
+
|
|
33
|
+
companion object {
|
|
34
|
+
private const val TAG = "BackgroundBlurModule"
|
|
35
|
+
const val NAME = "BackgroundBlurModule"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private var processor: BlurVideoProcessor? = null
|
|
39
|
+
private var imageProcessor: BackgroundImageProcessor? = null
|
|
40
|
+
private var videoSource: VideoSource? = null
|
|
41
|
+
|
|
42
|
+
override fun getName(): String = NAME
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Enable background blur on the local video stream.
|
|
46
|
+
*/
|
|
47
|
+
@ReactMethod
|
|
48
|
+
fun enableBlur(config: ReadableMap, promise: Promise) {
|
|
49
|
+
try {
|
|
50
|
+
val trackId = readConfigString(config, "trackId")
|
|
51
|
+
Log.d(TAG, "enableBlur called, trackId=$trackId")
|
|
52
|
+
|
|
53
|
+
if (trackId.isEmpty()) {
|
|
54
|
+
Log.e(TAG, "enableBlur rejected: trackId is empty — check that a video track is passed from JS")
|
|
55
|
+
promise.reject("INVALID_ARGS", "trackId is required")
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
val webRTCModule = reactApplicationContext
|
|
60
|
+
.getNativeModule(WebRTCModule::class.java)
|
|
61
|
+
if (webRTCModule == null) {
|
|
62
|
+
Log.e(TAG, "enableBlur rejected: WebRTCModule not found in ReactContext")
|
|
63
|
+
promise.reject("NO_WEBRTC", "WebRTCModule not found")
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
val source = findVideoSource(webRTCModule, trackId)
|
|
68
|
+
if (source == null) {
|
|
69
|
+
Log.e(TAG, "enableBlur rejected: VideoSource not found for trackId=$trackId")
|
|
70
|
+
promise.reject("NO_SOURCE", "VideoSource not found. Cannot attach blur processor.")
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
Log.d(TAG, "VideoSource found: $source")
|
|
75
|
+
|
|
76
|
+
if (processor != null) {
|
|
77
|
+
Log.d(TAG, "Replacing existing blur processor")
|
|
78
|
+
processor?.disable()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
val strong = try { config.getBoolean("strong") } catch (_: Exception) { false }
|
|
82
|
+
val proc = BlurVideoProcessor(
|
|
83
|
+
reactApplicationContext,
|
|
84
|
+
blurRadius = if (strong) 25f else 20f,
|
|
85
|
+
blurPasses = if (strong) 3 else 1,
|
|
86
|
+
)
|
|
87
|
+
proc.enable()
|
|
88
|
+
try {
|
|
89
|
+
source.setVideoProcessor(proc)
|
|
90
|
+
} catch (e: Exception) {
|
|
91
|
+
Log.e(TAG, "setVideoProcessor failed — VideoSource may be disposed", e)
|
|
92
|
+
proc.disable()
|
|
93
|
+
promise.reject("SOURCE_ERROR", "Failed to attach processor to VideoSource: ${e.message}", e)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
processor = proc
|
|
98
|
+
videoSource = source
|
|
99
|
+
|
|
100
|
+
Log.d(TAG, "Background blur enabled successfully")
|
|
101
|
+
promise.resolve(true)
|
|
102
|
+
|
|
103
|
+
} catch (e: Exception) {
|
|
104
|
+
Log.e(TAG, "Error enabling blur", e)
|
|
105
|
+
promise.reject("BLUR_ERROR", "Failed to enable blur: ${e.message}", e)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Disable background blur and restore normal video processing.
|
|
111
|
+
*/
|
|
112
|
+
@ReactMethod
|
|
113
|
+
fun disableBlur(promise: Promise) {
|
|
114
|
+
try {
|
|
115
|
+
Log.d(TAG, "disableBlur called")
|
|
116
|
+
|
|
117
|
+
videoSource?.setVideoProcessor(null)
|
|
118
|
+
processor?.disable()
|
|
119
|
+
processor = null
|
|
120
|
+
imageProcessor?.disable()
|
|
121
|
+
imageProcessor = null
|
|
122
|
+
videoSource = null
|
|
123
|
+
|
|
124
|
+
Log.d(TAG, "Video effect disabled successfully")
|
|
125
|
+
promise.resolve(true)
|
|
126
|
+
} catch (e: Exception) {
|
|
127
|
+
Log.e(TAG, "Error disabling video effect", e)
|
|
128
|
+
promise.reject("BLUR_ERROR", "Failed to disable effect: ${e.message}", e)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Enable background image replacement on the local video stream.
|
|
134
|
+
* Downloads the image from imageUrl synchronously on the native modules thread.
|
|
135
|
+
*/
|
|
136
|
+
@ReactMethod
|
|
137
|
+
fun enableBackgroundImage(config: ReadableMap, promise: Promise) {
|
|
138
|
+
try {
|
|
139
|
+
val trackId = readConfigString(config, "trackId")
|
|
140
|
+
val imageUrl = readConfigString(config, "imageUrl")
|
|
141
|
+
Log.d(TAG, "enableBackgroundImage called, trackId=$trackId")
|
|
142
|
+
|
|
143
|
+
if (trackId.isEmpty()) {
|
|
144
|
+
promise.reject("INVALID_ARGS", "trackId is required")
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
if (imageUrl.isEmpty()) {
|
|
148
|
+
promise.reject("INVALID_ARGS", "imageUrl is required")
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
val webRTCModule = reactApplicationContext.getNativeModule(WebRTCModule::class.java)
|
|
153
|
+
if (webRTCModule == null) {
|
|
154
|
+
promise.reject("NO_WEBRTC", "WebRTCModule not found")
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
val source = findVideoSource(webRTCModule, trackId)
|
|
159
|
+
if (source == null) {
|
|
160
|
+
promise.reject("NO_SOURCE", "VideoSource not found for trackId=$trackId")
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
val bitmap = downloadBitmap(imageUrl)
|
|
165
|
+
if (bitmap == null) {
|
|
166
|
+
promise.reject("DOWNLOAD_ERROR", "Failed to download image from $imageUrl")
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Disable any active effect first
|
|
171
|
+
videoSource?.setVideoProcessor(null)
|
|
172
|
+
processor?.disable()
|
|
173
|
+
processor = null
|
|
174
|
+
imageProcessor?.disable()
|
|
175
|
+
imageProcessor = null
|
|
176
|
+
|
|
177
|
+
val proc = BackgroundImageProcessor(bitmap)
|
|
178
|
+
proc.enable()
|
|
179
|
+
try {
|
|
180
|
+
source.setVideoProcessor(proc)
|
|
181
|
+
} catch (e: Exception) {
|
|
182
|
+
proc.disable()
|
|
183
|
+
promise.reject("SOURCE_ERROR", "Failed to attach processor: ${e.message}", e)
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
imageProcessor = proc
|
|
188
|
+
videoSource = source
|
|
189
|
+
|
|
190
|
+
Log.d(TAG, "Background image enabled successfully")
|
|
191
|
+
promise.resolve(true)
|
|
192
|
+
|
|
193
|
+
} catch (e: Exception) {
|
|
194
|
+
Log.e(TAG, "Error enabling background image", e)
|
|
195
|
+
promise.reject("IMAGE_BG_ERROR", "Failed to enable background image: ${e.message}", e)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check if any video effect (blur or background image) is currently active.
|
|
201
|
+
*/
|
|
202
|
+
@ReactMethod
|
|
203
|
+
fun isBlurEnabled(promise: Promise) {
|
|
204
|
+
promise.resolve(processor != null || imageProcessor != null)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Download a Bitmap from a URL synchronously (safe on the native modules background thread).
|
|
209
|
+
*/
|
|
210
|
+
private fun downloadBitmap(url: String): android.graphics.Bitmap? {
|
|
211
|
+
return try {
|
|
212
|
+
Log.d(TAG, "Downloading background image: $url")
|
|
213
|
+
val connection = java.net.URL(url).openConnection() as java.net.HttpURLConnection
|
|
214
|
+
connection.connectTimeout = 10_000
|
|
215
|
+
connection.readTimeout = 15_000
|
|
216
|
+
connection.doInput = true
|
|
217
|
+
connection.connect()
|
|
218
|
+
val bitmap = android.graphics.BitmapFactory.decodeStream(connection.inputStream)
|
|
219
|
+
connection.disconnect()
|
|
220
|
+
Log.d(TAG, "Downloaded: ${bitmap?.width}x${bitmap?.height}")
|
|
221
|
+
bitmap
|
|
222
|
+
} catch (e: Exception) {
|
|
223
|
+
Log.e(TAG, "Failed to download image from $url", e)
|
|
224
|
+
null
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// =====================================================================
|
|
229
|
+
// Reflection helpers to find VideoSource in WebRTCModule
|
|
230
|
+
// =====================================================================
|
|
231
|
+
|
|
232
|
+
private fun readConfigString(config: ReadableMap, key: String): String {
|
|
233
|
+
return try {
|
|
234
|
+
config.getString(key) ?: ""
|
|
235
|
+
} catch (_: Exception) {
|
|
236
|
+
try {
|
|
237
|
+
config.getDouble(key).toInt().toString()
|
|
238
|
+
} catch (_: Exception) {
|
|
239
|
+
""
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Find the VideoSource associated with the local camera track.
|
|
246
|
+
*
|
|
247
|
+
* Strategy:
|
|
248
|
+
* 1. WebRTCModule → getUserMediaImpl → tracks map → TrackPrivate.mediaSource
|
|
249
|
+
* 2. Deep reflection search for any VideoSource instance
|
|
250
|
+
*/
|
|
251
|
+
private fun findVideoSource(module: WebRTCModule, trackId: String): VideoSource? {
|
|
252
|
+
Log.d(TAG, "Searching for VideoSource, trackId=$trackId")
|
|
253
|
+
|
|
254
|
+
val gumiFieldNames = listOf("getUserMediaImpl", "mGetUserMediaImpl", "gumi", "getUserMedia")
|
|
255
|
+
for (gumiName in gumiFieldNames) {
|
|
256
|
+
try {
|
|
257
|
+
val gumiField = module.javaClass.getDeclaredField(gumiName)
|
|
258
|
+
gumiField.isAccessible = true
|
|
259
|
+
val gumi = gumiField.get(module) ?: continue
|
|
260
|
+
Log.d(TAG, "Found getUserMediaImpl via field '$gumiName': ${gumi.javaClass.name}")
|
|
261
|
+
|
|
262
|
+
val source = findVideoSourceInGetUserMediaImpl(gumi, trackId)
|
|
263
|
+
if (source != null) return source
|
|
264
|
+
} catch (_: NoSuchFieldException) {
|
|
265
|
+
} catch (e: Exception) {
|
|
266
|
+
Log.d(TAG, "Error accessing $gumiName: ${e.message}")
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
Log.d(TAG, "Falling back to deep reflection search from WebRTCModule")
|
|
271
|
+
val source = deepFindVideoSource(module, 4)
|
|
272
|
+
if (source != null) {
|
|
273
|
+
Log.d(TAG, "Found VideoSource via deep search")
|
|
274
|
+
return source
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
Log.e(TAG, "Could not find VideoSource through any strategy")
|
|
278
|
+
logFieldNames(module, "WebRTCModule")
|
|
279
|
+
return null
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private fun findVideoSourceInGetUserMediaImpl(gumi: Any, trackId: String): VideoSource? {
|
|
283
|
+
val captFieldNames = listOf("tracks", "mVideoCapturers", "videoCapturers", "capturers", "mCapturers")
|
|
284
|
+
for (captName in captFieldNames) {
|
|
285
|
+
try {
|
|
286
|
+
val captField = gumi.javaClass.getDeclaredField(captName)
|
|
287
|
+
captField.isAccessible = true
|
|
288
|
+
val capturers = captField.get(gumi)
|
|
289
|
+
|
|
290
|
+
if (capturers is Map<*, *>) {
|
|
291
|
+
Log.d(TAG, "Found capturers map '$captName' with ${capturers.size} entries, keys: ${capturers.keys}")
|
|
292
|
+
|
|
293
|
+
val exactMatch = capturers[trackId]
|
|
294
|
+
if (exactMatch == null) {
|
|
295
|
+
Log.w(TAG, "trackId='$trackId' not found in '$captName' map, falling back to first entry")
|
|
296
|
+
}
|
|
297
|
+
val controller = exactMatch ?: capturers.values.firstOrNull()
|
|
298
|
+
if (controller != null) {
|
|
299
|
+
val source = findVideoSourceInController(controller)
|
|
300
|
+
if (source != null) {
|
|
301
|
+
Log.d(TAG, "Found VideoSource in controller from '$captName'")
|
|
302
|
+
return source
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} catch (_: NoSuchFieldException) {
|
|
307
|
+
} catch (e: Exception) {
|
|
308
|
+
Log.d(TAG, "Error accessing $captName: ${e.message}")
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return deepFindVideoSource(gumi, 3)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private fun findVideoSourceInController(controller: Any): VideoSource? {
|
|
316
|
+
val srcFieldNames = listOf("mediaSource", "videoSource", "mVideoSource", "source", "mSource")
|
|
317
|
+
for (srcName in srcFieldNames) {
|
|
318
|
+
try {
|
|
319
|
+
val srcField = controller.javaClass.getDeclaredField(srcName)
|
|
320
|
+
srcField.isAccessible = true
|
|
321
|
+
val src = srcField.get(controller)
|
|
322
|
+
if (src is VideoSource) {
|
|
323
|
+
return src
|
|
324
|
+
}
|
|
325
|
+
} catch (_: NoSuchFieldException) {
|
|
326
|
+
} catch (e: Exception) {
|
|
327
|
+
Log.d(TAG, "Error accessing $srcName in controller: ${e.message}")
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return findFieldOfType(controller, VideoSource::class.java)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private fun <T> findFieldOfType(obj: Any, type: Class<T>): T? {
|
|
335
|
+
var clazz: Class<*>? = obj.javaClass
|
|
336
|
+
while (clazz != null && clazz != Any::class.java) {
|
|
337
|
+
for (field in clazz.declaredFields) {
|
|
338
|
+
try {
|
|
339
|
+
field.isAccessible = true
|
|
340
|
+
val value = field.get(obj)
|
|
341
|
+
if (type.isInstance(value)) {
|
|
342
|
+
@Suppress("UNCHECKED_CAST")
|
|
343
|
+
return value as T
|
|
344
|
+
}
|
|
345
|
+
} catch (_: Exception) {}
|
|
346
|
+
}
|
|
347
|
+
clazz = clazz.superclass
|
|
348
|
+
}
|
|
349
|
+
return null
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private fun deepFindVideoSource(root: Any, maxDepth: Int, visited: MutableSet<Int> = mutableSetOf()): VideoSource? {
|
|
353
|
+
if (maxDepth <= 0) return null
|
|
354
|
+
val id = System.identityHashCode(root)
|
|
355
|
+
if (!visited.add(id)) return null
|
|
356
|
+
|
|
357
|
+
var clazz: Class<*>? = root.javaClass
|
|
358
|
+
while (clazz != null && clazz != Any::class.java) {
|
|
359
|
+
for (field in clazz.declaredFields) {
|
|
360
|
+
try {
|
|
361
|
+
field.isAccessible = true
|
|
362
|
+
val value = field.get(root) ?: continue
|
|
363
|
+
|
|
364
|
+
if (value is VideoSource) {
|
|
365
|
+
Log.d(TAG, "deepFind: found VideoSource in ${root.javaClass.simpleName}.${field.name}")
|
|
366
|
+
return value
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (value is Map<*, *>) {
|
|
370
|
+
for (entry in value.values) {
|
|
371
|
+
if (entry == null) continue
|
|
372
|
+
if (entry is VideoSource) return entry
|
|
373
|
+
val found = deepFindVideoSource(entry, maxDepth - 1, visited)
|
|
374
|
+
if (found != null) return found
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (value is Collection<*>) {
|
|
379
|
+
for (item in value) {
|
|
380
|
+
if (item == null) continue
|
|
381
|
+
if (item is VideoSource) return item
|
|
382
|
+
val found = deepFindVideoSource(item, maxDepth - 1, visited)
|
|
383
|
+
if (found != null) return found
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
val pkg = value.javaClass.name
|
|
388
|
+
if (pkg.startsWith("com.oney") || pkg.startsWith("org.webrtc")) {
|
|
389
|
+
val found = deepFindVideoSource(value, maxDepth - 1, visited)
|
|
390
|
+
if (found != null) return found
|
|
391
|
+
}
|
|
392
|
+
} catch (_: Exception) {}
|
|
393
|
+
}
|
|
394
|
+
clazz = clazz.superclass
|
|
395
|
+
}
|
|
396
|
+
return null
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private fun logFieldNames(obj: Any, label: String) {
|
|
400
|
+
var clazz: Class<*>? = obj.javaClass
|
|
401
|
+
val fields = mutableListOf<String>()
|
|
402
|
+
while (clazz != null && clazz != Any::class.java) {
|
|
403
|
+
for (field in clazz.declaredFields) {
|
|
404
|
+
fields.add("${field.name}: ${field.type.simpleName}")
|
|
405
|
+
}
|
|
406
|
+
clazz = clazz.superclass
|
|
407
|
+
}
|
|
408
|
+
Log.d(TAG, "$label fields: $fields")
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
override fun onCatalystInstanceDestroy() {
|
|
412
|
+
super.onCatalystInstanceDestroy()
|
|
413
|
+
videoSource?.setVideoProcessor(null)
|
|
414
|
+
processor?.disable()
|
|
415
|
+
processor = null
|
|
416
|
+
imageProcessor?.disable()
|
|
417
|
+
imageProcessor = null
|
|
418
|
+
videoSource = null
|
|
419
|
+
}
|
|
420
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package com.reactnativeapirtc
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class BackgroundBlurPackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
10
|
+
return listOf(BackgroundBlurModule(reactContext))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
14
|
+
return emptyList()
|
|
15
|
+
}
|
|
16
|
+
}
|