omikit-plugin 4.0.2 → 4.1.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/README.md +654 -37
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/omikitplugin/OmiLocalCameraView.kt +94 -0
- package/android/src/main/java/com/omikitplugin/OmiRemoteCameraView.kt +117 -0
- package/android/src/main/java/com/omikitplugin/OmikitPluginModule.kt +24 -17
- package/android/src/main/java/com/omikitplugin/OmikitPluginPackage.kt +11 -8
- package/ios/CallProcess/CallManager.swift +99 -29
- package/ios/Library/OmikitPlugin.m +18 -0
- package/ios/Library/OmikitPlugin.swift +233 -1
- package/ios/OmikitPlugin-Bridging-Header.h +1 -0
- package/ios/OmikitPlugin.xcodeproj/project.pbxproj +4 -4
- package/ios/VideoCall/OmiLocalCameraViewBridge.m +14 -0
- package/ios/VideoCall/OmiLocalCameraViewManager.swift +41 -0
- package/ios/VideoCall/OmiRemoteCameraViewBridge.m +14 -0
- package/ios/VideoCall/OmiRemoteCameraViewManager.swift +40 -0
- package/lib/commonjs/NativeOmikitPlugin.js +2 -1
- package/lib/commonjs/NativeOmikitPlugin.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/omi_audio_type.js +5 -7
- package/lib/commonjs/omi_audio_type.js.map +1 -1
- package/lib/commonjs/omi_call_state.js +5 -3
- package/lib/commonjs/omi_call_state.js.map +1 -1
- package/lib/commonjs/omi_local_camera.js +19 -17
- package/lib/commonjs/omi_local_camera.js.map +1 -1
- package/lib/commonjs/omi_remote_camera.js +20 -17
- package/lib/commonjs/omi_remote_camera.js.map +1 -1
- package/lib/commonjs/omi_start_call_status.js +5 -24
- package/lib/commonjs/omi_start_call_status.js.map +1 -1
- package/lib/commonjs/omikit.js +56 -3
- package/lib/commonjs/omikit.js.map +1 -1
- package/lib/commonjs/types/index.d.js.map +1 -1
- package/lib/module/NativeOmikitPlugin.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/omi_audio_type.js +4 -7
- package/lib/module/omi_audio_type.js.map +1 -1
- package/lib/module/omi_call_state.js +4 -3
- package/lib/module/omi_call_state.js.map +1 -1
- package/lib/module/omi_local_camera.js +19 -18
- package/lib/module/omi_local_camera.js.map +1 -1
- package/lib/module/omi_remote_camera.js +20 -18
- package/lib/module/omi_remote_camera.js.map +1 -1
- package/lib/module/omi_start_call_status.js +4 -24
- package/lib/module/omi_start_call_status.js.map +1 -1
- package/lib/module/omikit.js +49 -1
- package/lib/module/omikit.js.map +1 -1
- package/lib/module/types/index.d.js.map +1 -1
- package/omikit-plugin.podspec +1 -1
- package/package.json +2 -11
- package/react-native.config.js +14 -0
- package/src/NativeOmikitPlugin.ts +1 -0
- package/src/omi_call_state.tsx +1 -0
- package/src/omi_local_camera.tsx +15 -19
- package/src/omi_remote_camera.tsx +16 -19
- package/src/omikit.tsx +63 -0
- package/src/types/index.d.ts +344 -62
- package/android/src/main/java/com/omikitplugin/FLLocalCameraModule.kt +0 -34
- package/android/src/main/java/com/omikitplugin/FLLocalCameraView.kt +0 -44
- package/android/src/main/java/com/omikitplugin/FLRemoteCameraModule.kt +0 -37
- package/android/src/main/java/com/omikitplugin/FLRemoteCameraView.kt +0 -23
- package/ios/VideoCall/FLLocalCameraView.m +0 -17
- package/ios/VideoCall/FLLocalCameraView.swift +0 -44
- package/ios/VideoCall/FLRemoteCameraView.m +0 -18
- package/ios/VideoCall/FLRemoteCameraView.swift +0 -124
package/android/build.gradle
CHANGED
|
@@ -65,7 +65,7 @@ dependencies {
|
|
|
65
65
|
// OMISDK
|
|
66
66
|
implementation("androidx.work:work-runtime:2.8.1")
|
|
67
67
|
implementation "androidx.security:security-crypto:1.1.0-alpha06"
|
|
68
|
-
api "io.omicrm.vihat:omi-sdk:2.6.
|
|
68
|
+
api "io.omicrm.vihat:omi-sdk:2.6.6"
|
|
69
69
|
|
|
70
70
|
// React Native — resolved from consumer's node_modules
|
|
71
71
|
implementation "com.facebook.react:react-native:+"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
package com.omikitplugin
|
|
2
|
+
|
|
3
|
+
import android.graphics.SurfaceTexture
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import android.view.Surface
|
|
6
|
+
import android.view.TextureView
|
|
7
|
+
import android.view.ViewGroup
|
|
8
|
+
import android.widget.FrameLayout
|
|
9
|
+
import com.facebook.react.bridge.Promise
|
|
10
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
11
|
+
import com.facebook.react.bridge.ReactMethod
|
|
12
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
13
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
14
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
15
|
+
import vn.vihat.omicall.omisdk.OmiClient
|
|
16
|
+
import vn.vihat.omicall.omisdk.videoutils.ScaleManager
|
|
17
|
+
import vn.vihat.omicall.omisdk.videoutils.Size
|
|
18
|
+
|
|
19
|
+
class OmiLocalCameraView(private val context: ReactApplicationContext) :
|
|
20
|
+
SimpleViewManager<FrameLayout>() {
|
|
21
|
+
|
|
22
|
+
val localView: FrameLayout = FrameLayout(context)
|
|
23
|
+
private val cameraView: TextureView = TextureView(context)
|
|
24
|
+
|
|
25
|
+
@Volatile
|
|
26
|
+
private var isSurfaceReady = false
|
|
27
|
+
private var pendingRefreshPromise: Promise? = null
|
|
28
|
+
|
|
29
|
+
init {
|
|
30
|
+
// TextureView fills container — RN styles (width/height) control the FrameLayout
|
|
31
|
+
localView.addView(cameraView, FrameLayout.LayoutParams(
|
|
32
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
33
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
34
|
+
))
|
|
35
|
+
|
|
36
|
+
cameraView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
|
|
37
|
+
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
|
|
38
|
+
isSurfaceReady = true
|
|
39
|
+
pendingRefreshPromise?.let { promise ->
|
|
40
|
+
pendingRefreshPromise = null
|
|
41
|
+
doRefresh(promise)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {}
|
|
46
|
+
|
|
47
|
+
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
|
|
48
|
+
isSurfaceReady = false
|
|
49
|
+
pendingRefreshPromise = null
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override fun getName(): String = "OmiLocalCameraView"
|
|
58
|
+
|
|
59
|
+
override fun createViewInstance(p0: ThemedReactContext): FrameLayout {
|
|
60
|
+
(localView.parent as? ViewGroup)?.removeView(localView)
|
|
61
|
+
return localView
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fun localViewInstance(): FrameLayout = localView
|
|
65
|
+
|
|
66
|
+
@ReactMethod
|
|
67
|
+
fun refresh(promise: Promise) {
|
|
68
|
+
UiThreadUtil.runOnUiThread {
|
|
69
|
+
if (isSurfaceReady && cameraView.surfaceTexture != null) {
|
|
70
|
+
doRefresh(promise)
|
|
71
|
+
} else {
|
|
72
|
+
pendingRefreshPromise = promise
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private fun doRefresh(promise: Promise) {
|
|
78
|
+
try {
|
|
79
|
+
val surface = Surface(cameraView.surfaceTexture)
|
|
80
|
+
OmiClient.getInstance(context.applicationContext).setupLocalVideoFeed(surface)
|
|
81
|
+
Log.d("OmiLocalCameraView", "Connected local video feed to surface")
|
|
82
|
+
|
|
83
|
+
ScaleManager.adjustAspectRatioCrop(
|
|
84
|
+
cameraView,
|
|
85
|
+
Size(cameraView.width, cameraView.height),
|
|
86
|
+
Size(3, 4)
|
|
87
|
+
)
|
|
88
|
+
promise.resolve(true)
|
|
89
|
+
} catch (e: Exception) {
|
|
90
|
+
Log.e("OmiLocalCameraView", "Error refreshing: ${e.message}")
|
|
91
|
+
promise.resolve(false)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
package com.omikitplugin
|
|
2
|
+
|
|
3
|
+
import android.graphics.SurfaceTexture
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import android.view.Surface
|
|
6
|
+
import android.view.TextureView
|
|
7
|
+
import android.view.ViewGroup
|
|
8
|
+
import android.widget.FrameLayout
|
|
9
|
+
import com.facebook.react.bridge.Promise
|
|
10
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
11
|
+
import com.facebook.react.bridge.ReactMethod
|
|
12
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
13
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
14
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
15
|
+
import vn.vihat.omicall.omisdk.OmiClient
|
|
16
|
+
import vn.vihat.omicall.omisdk.videoutils.ScaleManager
|
|
17
|
+
import vn.vihat.omicall.omisdk.videoutils.Size
|
|
18
|
+
|
|
19
|
+
class OmiRemoteCameraView(private val context: ReactApplicationContext) :
|
|
20
|
+
SimpleViewManager<FrameLayout>() {
|
|
21
|
+
|
|
22
|
+
companion object {
|
|
23
|
+
@Volatile
|
|
24
|
+
var instance: OmiRemoteCameraView? = null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
val remoteContainer: FrameLayout = FrameLayout(context)
|
|
28
|
+
private val remoteView: TextureView = TextureView(context)
|
|
29
|
+
|
|
30
|
+
@Volatile
|
|
31
|
+
private var isSurfaceReady = false
|
|
32
|
+
private var pendingRefreshPromise: Promise? = null
|
|
33
|
+
|
|
34
|
+
init {
|
|
35
|
+
instance = this
|
|
36
|
+
remoteContainer.addView(remoteView, FrameLayout.LayoutParams(
|
|
37
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
38
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
39
|
+
))
|
|
40
|
+
|
|
41
|
+
remoteView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
|
|
42
|
+
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
|
|
43
|
+
isSurfaceReady = true
|
|
44
|
+
pendingRefreshPromise?.let { promise ->
|
|
45
|
+
pendingRefreshPromise = null
|
|
46
|
+
doRefresh(promise)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {}
|
|
51
|
+
|
|
52
|
+
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
|
|
53
|
+
isSurfaceReady = false
|
|
54
|
+
pendingRefreshPromise = null
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override fun getName(): String = "OmiRemoteCameraView"
|
|
63
|
+
|
|
64
|
+
override fun createViewInstance(p0: ThemedReactContext): FrameLayout {
|
|
65
|
+
(remoteContainer.parent as? ViewGroup)?.removeView(remoteContainer)
|
|
66
|
+
return remoteContainer
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fun remoteViewInstance(): FrameLayout = remoteContainer
|
|
70
|
+
|
|
71
|
+
@ReactMethod
|
|
72
|
+
fun refresh(promise: Promise) {
|
|
73
|
+
UiThreadUtil.runOnUiThread {
|
|
74
|
+
if (isSurfaceReady && remoteView.surfaceTexture != null) {
|
|
75
|
+
doRefresh(promise)
|
|
76
|
+
} else {
|
|
77
|
+
pendingRefreshPromise = promise
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private fun doRefresh(promise: Promise) {
|
|
83
|
+
try {
|
|
84
|
+
val surface = Surface(remoteView.surfaceTexture)
|
|
85
|
+
OmiClient.getInstance(context.applicationContext).setupIncomingVideoFeed(surface)
|
|
86
|
+
Log.d("OmiRemoteCameraView", "Connected remote video feed to surface")
|
|
87
|
+
|
|
88
|
+
// Default landscape; updated by onVideoSize when PJSIP reports actual dimensions
|
|
89
|
+
ScaleManager.adjustAspectRatio(
|
|
90
|
+
remoteView,
|
|
91
|
+
Size(remoteView.width, remoteView.height),
|
|
92
|
+
Size(640, 480)
|
|
93
|
+
)
|
|
94
|
+
promise.resolve(true)
|
|
95
|
+
} catch (e: Exception) {
|
|
96
|
+
Log.e("OmiRemoteCameraView", "Error refreshing: ${e.message}")
|
|
97
|
+
promise.resolve(false)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Called from OmikitPluginModule.onVideoSize() when PJSIP reports
|
|
103
|
+
* actual remote video dimensions. Re-applies correct aspect ratio.
|
|
104
|
+
*/
|
|
105
|
+
fun updateAspectRatio(videoWidth: Int, videoHeight: Int) {
|
|
106
|
+
UiThreadUtil.runOnUiThread {
|
|
107
|
+
if (remoteView.width > 0 && remoteView.height > 0 && videoWidth > 0 && videoHeight > 0) {
|
|
108
|
+
Log.d("OmiRemoteCameraView", "updateAspectRatio: video=${videoWidth}x${videoHeight}")
|
|
109
|
+
ScaleManager.adjustAspectRatio(
|
|
110
|
+
remoteView,
|
|
111
|
+
Size(remoteView.width, remoteView.height),
|
|
112
|
+
Size(videoWidth, videoHeight)
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -121,6 +121,11 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
|
|
|
121
121
|
private var isIncoming: Boolean = false
|
|
122
122
|
private var isAnswerCall: Boolean = false
|
|
123
123
|
@Volatile private var permissionPromise: Promise? = null
|
|
124
|
+
|
|
125
|
+
// Helper for bridgeless mode (Expo/RN 0.81+) where currentActivity
|
|
126
|
+
// is not directly available as inherited property
|
|
127
|
+
private val safeActivity: Activity?
|
|
128
|
+
get() = reactApplicationContext?.currentActivity
|
|
124
129
|
|
|
125
130
|
// Call state management to prevent concurrent calls
|
|
126
131
|
private var isCallInProgress: Boolean = false
|
|
@@ -409,7 +414,8 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
|
|
|
409
414
|
}
|
|
410
415
|
|
|
411
416
|
override fun onVideoSize(width: Int, height: Int) {
|
|
412
|
-
|
|
417
|
+
// PJSIP reports actual remote video dimensions — update aspect ratio dynamically
|
|
418
|
+
OmiRemoteCameraView.instance?.updateAspectRatio(width, height)
|
|
413
419
|
}
|
|
414
420
|
|
|
415
421
|
private val accountListener = object : OmiAccountListener {
|
|
@@ -450,7 +456,9 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
|
|
|
450
456
|
moduleInstance = this
|
|
451
457
|
reactApplicationContext!!.addActivityEventListener(this)
|
|
452
458
|
Handler(Looper.getMainLooper()).post {
|
|
453
|
-
|
|
459
|
+
// Use applicationContext — pjsip video subsystem needs it for CameraManager access
|
|
460
|
+
val ctx = reactApplicationContext?.applicationContext ?: reactApplicationContext!!
|
|
461
|
+
val client = OmiClient.getInstance(ctx)
|
|
454
462
|
client.addCallStateListener(this)
|
|
455
463
|
client.addCallStateListener(autoUnregisterListener)
|
|
456
464
|
client.setDebug(false)
|
|
@@ -463,10 +471,9 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
|
|
|
463
471
|
try {
|
|
464
472
|
// ✅ Prepare audio system trước khi start services
|
|
465
473
|
prepareAudioSystem()
|
|
466
|
-
|
|
474
|
+
|
|
467
475
|
OmiClient.getInstance(reactApplicationContext!!).addAccountListener(accountListener)
|
|
468
|
-
|
|
469
|
-
// ✅ Start services - không cần prevent auto-unregister với Silent API
|
|
476
|
+
|
|
470
477
|
OmiClient.getInstance(reactApplicationContext!!).setDebug(false)
|
|
471
478
|
promise.resolve(true)
|
|
472
479
|
} catch (e: Exception) {
|
|
@@ -632,7 +639,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
|
|
|
632
639
|
return
|
|
633
640
|
}
|
|
634
641
|
|
|
635
|
-
|
|
642
|
+
safeActivity?.runOnUiThread {
|
|
636
643
|
try {
|
|
637
644
|
// Extract parameters from data with proper defaults
|
|
638
645
|
val notificationIcon = data.getString("notificationIcon") ?: "ic_notification"
|
|
@@ -897,7 +904,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
|
|
|
897
904
|
)
|
|
898
905
|
val map: WritableMap = WritableNativeMap()
|
|
899
906
|
if (audio == PackageManager.PERMISSION_GRANTED) {
|
|
900
|
-
val activity =
|
|
907
|
+
val activity = safeActivity
|
|
901
908
|
if (activity == null) {
|
|
902
909
|
promise.reject("E_NO_ACTIVITY", "Current activity is null")
|
|
903
910
|
return
|
|
@@ -965,7 +972,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
|
|
|
965
972
|
@ReactMethod
|
|
966
973
|
fun joinCall(promise: Promise) {
|
|
967
974
|
val appContext = reactApplicationContext.applicationContext
|
|
968
|
-
val activity =
|
|
975
|
+
val activity = safeActivity
|
|
969
976
|
|
|
970
977
|
if (appContext == null) {
|
|
971
978
|
promise.reject("E_NULL_CONTEXT", "Application context is null")
|
|
@@ -1074,7 +1081,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
|
|
|
1074
1081
|
|
|
1075
1082
|
@ReactMethod
|
|
1076
1083
|
fun toggleSpeaker(promise: Promise) {
|
|
1077
|
-
val activity =
|
|
1084
|
+
val activity = safeActivity
|
|
1078
1085
|
if (activity == null) { promise.resolve(null); return }
|
|
1079
1086
|
activity.runOnUiThread {
|
|
1080
1087
|
val newStatus = OmiClient.getInstance(reactApplicationContext!!).toggleSpeaker()
|
|
@@ -1085,7 +1092,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
|
|
|
1085
1092
|
|
|
1086
1093
|
@ReactMethod
|
|
1087
1094
|
fun sendDTMF(data: ReadableMap, promise: Promise) {
|
|
1088
|
-
val activity =
|
|
1095
|
+
val activity = safeActivity
|
|
1089
1096
|
if (activity == null) { promise.resolve(false); return }
|
|
1090
1097
|
activity.runOnUiThread {
|
|
1091
1098
|
val character = data.getString("character")
|
|
@@ -1105,7 +1112,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
|
|
|
1105
1112
|
|
|
1106
1113
|
@ReactMethod
|
|
1107
1114
|
fun switchOmiCamera(promise: Promise) {
|
|
1108
|
-
val activity =
|
|
1115
|
+
val activity = safeActivity
|
|
1109
1116
|
if (activity == null) { promise.resolve(false); return }
|
|
1110
1117
|
activity.runOnUiThread {
|
|
1111
1118
|
OmiClient.getInstance(reactApplicationContext!!).switchCamera()
|
|
@@ -1115,7 +1122,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
|
|
|
1115
1122
|
|
|
1116
1123
|
@ReactMethod
|
|
1117
1124
|
fun toggleOmiVideo(promise: Promise) {
|
|
1118
|
-
val activity =
|
|
1125
|
+
val activity = safeActivity
|
|
1119
1126
|
if (activity == null) { promise.resolve(false); return }
|
|
1120
1127
|
activity.runOnUiThread {
|
|
1121
1128
|
OmiClient.getInstance(reactApplicationContext!!).toggleCamera()
|
|
@@ -1353,7 +1360,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
|
|
|
1353
1360
|
|
|
1354
1361
|
@ReactMethod
|
|
1355
1362
|
fun transferCall(data: ReadableMap, promise: Promise) {
|
|
1356
|
-
val activity =
|
|
1363
|
+
val activity = safeActivity
|
|
1357
1364
|
if (activity == null) { promise.resolve(false); return }
|
|
1358
1365
|
activity.runOnUiThread {
|
|
1359
1366
|
val phone = data.getString("phoneNumber")
|
|
@@ -1558,7 +1565,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
|
|
|
1558
1565
|
// Store promise for callback
|
|
1559
1566
|
permissionPromise = promise
|
|
1560
1567
|
|
|
1561
|
-
val activity =
|
|
1568
|
+
val activity = safeActivity ?: run {
|
|
1562
1569
|
promise.resolve(false)
|
|
1563
1570
|
return
|
|
1564
1571
|
}
|
|
@@ -1680,7 +1687,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
|
|
|
1680
1687
|
Uri.parse("package:${reactApplicationContext.packageName}")
|
|
1681
1688
|
)
|
|
1682
1689
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
1683
|
-
|
|
1690
|
+
safeActivity?.startActivityForResult(intent, REQUEST_OVERLAY_PERMISSION_CODE)
|
|
1684
1691
|
} else {
|
|
1685
1692
|
promise.resolve(true)
|
|
1686
1693
|
}
|
|
@@ -1736,7 +1743,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
|
|
|
1736
1743
|
// Store promise for callback
|
|
1737
1744
|
permissionPromise = promise
|
|
1738
1745
|
|
|
1739
|
-
val activity =
|
|
1746
|
+
val activity = safeActivity ?: run {
|
|
1740
1747
|
promise.reject("E_NULL_ACTIVITY", "Current activity is null")
|
|
1741
1748
|
return
|
|
1742
1749
|
}
|
|
@@ -1758,7 +1765,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
|
|
|
1758
1765
|
return
|
|
1759
1766
|
}
|
|
1760
1767
|
|
|
1761
|
-
val activity =
|
|
1768
|
+
val activity = safeActivity ?: return
|
|
1762
1769
|
ActivityCompat.requestPermissions(
|
|
1763
1770
|
activity,
|
|
1764
1771
|
missingPermissions.toTypedArray(),
|
|
@@ -8,28 +8,31 @@ import com.facebook.react.uimanager.ViewManager
|
|
|
8
8
|
|
|
9
9
|
class OmikitPluginPackage : ReactPackage {
|
|
10
10
|
|
|
11
|
-
private var localView:
|
|
12
|
-
private var remoteView:
|
|
11
|
+
private var localView: OmiLocalCameraView? = null
|
|
12
|
+
private var remoteView: OmiRemoteCameraView? = null
|
|
13
|
+
|
|
13
14
|
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
14
15
|
if (localView == null) {
|
|
15
|
-
localView =
|
|
16
|
+
localView = OmiLocalCameraView(reactContext)
|
|
16
17
|
}
|
|
17
18
|
if (remoteView == null) {
|
|
18
|
-
remoteView =
|
|
19
|
+
remoteView = OmiRemoteCameraView(reactContext)
|
|
19
20
|
}
|
|
21
|
+
// ViewManagers are also NativeModules — refresh() is accessible via
|
|
22
|
+
// NativeModules.OmiLocalCameraView and NativeModules.OmiRemoteCameraView
|
|
20
23
|
return listOf(
|
|
21
24
|
OmikitPluginModule(reactContext),
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
localView!!,
|
|
26
|
+
remoteView!!,
|
|
24
27
|
)
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
28
31
|
if (localView == null) {
|
|
29
|
-
localView =
|
|
32
|
+
localView = OmiLocalCameraView(reactContext)
|
|
30
33
|
}
|
|
31
34
|
if (remoteView == null) {
|
|
32
|
-
remoteView =
|
|
35
|
+
remoteView = OmiRemoteCameraView(reactContext)
|
|
33
36
|
}
|
|
34
37
|
return listOf(localView!!, remoteView!!)
|
|
35
38
|
}
|
|
@@ -12,6 +12,14 @@ import SwiftUI
|
|
|
12
12
|
import OmiKit
|
|
13
13
|
import AVFoundation
|
|
14
14
|
|
|
15
|
+
// UIWindow that passes all touches through to the window underneath
|
|
16
|
+
class PassthroughWindow: UIWindow {
|
|
17
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
18
|
+
// Always return nil — all touches pass through to React window below
|
|
19
|
+
return nil
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
class CallManager {
|
|
16
24
|
|
|
17
25
|
static private var instance: CallManager? = nil // Instance
|
|
@@ -19,12 +27,39 @@ class CallManager {
|
|
|
19
27
|
private lazy var omiLib: OMISIPLib = {
|
|
20
28
|
return OMISIPLib.sharedInstance()
|
|
21
29
|
}()
|
|
22
|
-
var videoManager: OMIVideoViewManager?
|
|
23
30
|
var isSpeaker = false
|
|
31
|
+
// Container views for video — created lazily, strong reference to keep alive
|
|
32
|
+
var remoteContainerView: UIView?
|
|
33
|
+
var localContainerView: UIView?
|
|
34
|
+
// Separate UIWindow for video (fallback only)
|
|
35
|
+
var videoWindow: UIWindow?
|
|
36
|
+
private var isVideoSetup = false
|
|
37
|
+
private var setupVideoRetryCount = 0
|
|
24
38
|
private var guestPhone : String = ""
|
|
25
39
|
private var lastStatusCall : String?
|
|
26
40
|
private var tempCallInfo : [String: Any]?
|
|
27
41
|
private var lastTimeCall : Date = Date()
|
|
42
|
+
// Store original backgrounds to restore after video cleanup
|
|
43
|
+
private var savedBackgrounds: [(UIView, UIColor?)] = []
|
|
44
|
+
|
|
45
|
+
// Recursively make all views transparent, saving original colors
|
|
46
|
+
static func makeViewHierarchyTransparent(_ view: UIView) {
|
|
47
|
+
let manager = CallManager.shareInstance()
|
|
48
|
+
manager.savedBackgrounds.append((view, view.backgroundColor))
|
|
49
|
+
view.backgroundColor = .clear
|
|
50
|
+
for child in view.subviews {
|
|
51
|
+
makeViewHierarchyTransparent(child)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Restore saved backgrounds
|
|
56
|
+
func restoreSavedBackgrounds() {
|
|
57
|
+
for (view, color) in savedBackgrounds {
|
|
58
|
+
view.backgroundColor = color
|
|
59
|
+
}
|
|
60
|
+
savedBackgrounds.removeAll()
|
|
61
|
+
}
|
|
62
|
+
|
|
28
63
|
/// Get instance
|
|
29
64
|
static func shareInstance() -> CallManager {
|
|
30
65
|
if (instance == nil) {
|
|
@@ -281,13 +316,29 @@ class CallManager {
|
|
|
281
316
|
name: NSNotification.Name.OMICallVideoInfo,
|
|
282
317
|
object: nil
|
|
283
318
|
)
|
|
319
|
+
// Observe app foreground for video recovery (BG→FG)
|
|
320
|
+
NotificationCenter.default.addObserver(instance,
|
|
321
|
+
selector: #selector(self.appDidBecomeActive),
|
|
322
|
+
name: UIApplication.didBecomeActiveNotification,
|
|
323
|
+
object: nil
|
|
324
|
+
)
|
|
284
325
|
}
|
|
285
326
|
}
|
|
286
|
-
|
|
327
|
+
|
|
287
328
|
func removeVideoEvent() {
|
|
288
|
-
DispatchQueue.main.async {
|
|
329
|
+
DispatchQueue.main.async { [weak self] in
|
|
289
330
|
guard let instance = CallManager.instance else { return }
|
|
290
331
|
NotificationCenter.default.removeObserver(instance, name: NSNotification.Name.OMICallVideoInfo, object: nil)
|
|
332
|
+
NotificationCenter.default.removeObserver(instance, name: UIApplication.didBecomeActiveNotification, object: nil)
|
|
333
|
+
// Cleanup video when events are removed (screen dismissed)
|
|
334
|
+
self?.cleanupVideo()
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
@objc func appDidBecomeActive() {
|
|
339
|
+
// Recover video after background → foreground transition
|
|
340
|
+
if isVideoSetup {
|
|
341
|
+
OMIVideoCallManager.shared().prepareForVideoDisplay()
|
|
291
342
|
}
|
|
292
343
|
}
|
|
293
344
|
|
|
@@ -385,8 +436,8 @@ class CallManager {
|
|
|
385
436
|
|
|
386
437
|
switch (callState) {
|
|
387
438
|
case OMICallState.confirmed.rawValue:
|
|
388
|
-
if
|
|
389
|
-
|
|
439
|
+
if call.isVideo {
|
|
440
|
+
setupVideo()
|
|
390
441
|
}
|
|
391
442
|
isSpeaker = call.speaker
|
|
392
443
|
lastStatusCall = "answered"
|
|
@@ -397,9 +448,7 @@ class CallManager {
|
|
|
397
448
|
break
|
|
398
449
|
case OMICallState.disconnected.rawValue:
|
|
399
450
|
tempCallInfo = getCallInfo(call: call)
|
|
400
|
-
|
|
401
|
-
videoManager = nil
|
|
402
|
-
}
|
|
451
|
+
cleanupVideo()
|
|
403
452
|
lastStatusCall = nil
|
|
404
453
|
guestPhone = ""
|
|
405
454
|
var combinedDictionary: [String: Any] = dataToSend
|
|
@@ -596,32 +645,53 @@ func startCall(_ phoneNumber: String, isVideo: Bool, completion: @escaping (_: S
|
|
|
596
645
|
return OmiClient.getCurrentAudio()
|
|
597
646
|
}
|
|
598
647
|
|
|
599
|
-
//
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
648
|
+
// MARK: - Video Call (OMIVideoCallManager API)
|
|
649
|
+
|
|
650
|
+
/// Setup video — only succeeds if containers are already set and in window.
|
|
651
|
+
/// Does NOT defer or retry. Call setupVideoWithContainers() to create + setup in one step.
|
|
652
|
+
@objc func setupVideo() {
|
|
653
|
+
guard !isVideoSetup else {
|
|
654
|
+
NSLog("📹 [RN-CallManager] setupVideo: already setup, skipping")
|
|
655
|
+
return
|
|
603
656
|
}
|
|
657
|
+
guard let remote = self.remoteContainerView,
|
|
658
|
+
let local = self.localContainerView,
|
|
659
|
+
remote.window != nil else {
|
|
660
|
+
NSLog("📹 [RN-CallManager] setupVideo: containers not ready or not in window")
|
|
661
|
+
return
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
NSLog("📹 [RN-CallManager] setupVideo: calling OMIVideoCallManager.setupWithRemoteView")
|
|
665
|
+
OMIVideoCallManager.shared().setup(withRemoteView: remote, localView: local)
|
|
666
|
+
self.isVideoSetup = true
|
|
604
667
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
668
|
+
|
|
669
|
+
/// Cleanup video resources
|
|
670
|
+
func cleanupVideo() {
|
|
671
|
+
if isVideoSetup {
|
|
672
|
+
OMIVideoCallManager.shared().cleanup()
|
|
673
|
+
isVideoSetup = false
|
|
674
|
+
}
|
|
675
|
+
// Remove containers from window and clear references
|
|
676
|
+
DispatchQueue.main.async { [weak self] in
|
|
677
|
+
self?.remoteContainerView?.removeFromSuperview()
|
|
678
|
+
self?.localContainerView?.removeFromSuperview()
|
|
679
|
+
self?.remoteContainerView = nil
|
|
680
|
+
self?.localContainerView = nil
|
|
681
|
+
NSLog("📹 [RN-CallManager] cleanupVideo: removed video views from window")
|
|
614
682
|
}
|
|
615
683
|
}
|
|
616
|
-
|
|
617
|
-
func
|
|
618
|
-
|
|
619
|
-
return videoManager.createView(forVideoLocal: frame)
|
|
684
|
+
|
|
685
|
+
func toggleCamera() {
|
|
686
|
+
OMIVideoCallManager.shared().toggleCamera()
|
|
620
687
|
}
|
|
621
|
-
|
|
622
|
-
func
|
|
623
|
-
|
|
624
|
-
|
|
688
|
+
|
|
689
|
+
func getCameraStatus() -> Bool {
|
|
690
|
+
return OMIVideoCallManager.shared().isCameraOn
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
func switchCamera() {
|
|
694
|
+
OMIVideoCallManager.shared().switchCamera()
|
|
625
695
|
}
|
|
626
696
|
|
|
627
697
|
func logout() {
|
|
@@ -65,6 +65,15 @@ RCT_EXTERN_METHOD(sendDTMF:(id)data
|
|
|
65
65
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
66
66
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
67
67
|
|
|
68
|
+
// Configure camera view style (iOS Fabric — native window rendering)
|
|
69
|
+
RCT_EXTERN_METHOD(setCameraConfig:(NSDictionary *)data
|
|
70
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
71
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
72
|
+
|
|
73
|
+
// Setup video containers (iOS Fabric — creates and adds to window)
|
|
74
|
+
RCT_EXTERN_METHOD(setupVideoContainers:(RCTPromiseResolveBlock)resolve
|
|
75
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
76
|
+
|
|
68
77
|
// Switch camera
|
|
69
78
|
RCT_EXTERN_METHOD(switchOmiCamera:(RCTPromiseResolveBlock)resolve
|
|
70
79
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
@@ -81,6 +90,15 @@ RCT_EXTERN_METHOD(logout:(RCTPromiseResolveBlock)resolve
|
|
|
81
90
|
RCT_EXTERN_METHOD(registerVideoEvent:(RCTPromiseResolveBlock)resolve
|
|
82
91
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
83
92
|
|
|
93
|
+
// Attach video containers to React views by nativeID (Fabric interop)
|
|
94
|
+
RCT_EXTERN_METHOD(attachRemoteView:(NSString *)nativeID
|
|
95
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
96
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
97
|
+
|
|
98
|
+
RCT_EXTERN_METHOD(attachLocalView:(NSString *)nativeID
|
|
99
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
100
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
101
|
+
|
|
84
102
|
// Remove video event
|
|
85
103
|
RCT_EXTERN_METHOD(removeVideoEvent:(RCTPromiseResolveBlock)resolve
|
|
86
104
|
rejecter:(RCTPromiseRejectBlock)reject)
|