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.
Files changed (63) hide show
  1. package/README.md +654 -37
  2. package/android/build.gradle +1 -1
  3. package/android/src/main/java/com/omikitplugin/OmiLocalCameraView.kt +94 -0
  4. package/android/src/main/java/com/omikitplugin/OmiRemoteCameraView.kt +117 -0
  5. package/android/src/main/java/com/omikitplugin/OmikitPluginModule.kt +24 -17
  6. package/android/src/main/java/com/omikitplugin/OmikitPluginPackage.kt +11 -8
  7. package/ios/CallProcess/CallManager.swift +99 -29
  8. package/ios/Library/OmikitPlugin.m +18 -0
  9. package/ios/Library/OmikitPlugin.swift +233 -1
  10. package/ios/OmikitPlugin-Bridging-Header.h +1 -0
  11. package/ios/OmikitPlugin.xcodeproj/project.pbxproj +4 -4
  12. package/ios/VideoCall/OmiLocalCameraViewBridge.m +14 -0
  13. package/ios/VideoCall/OmiLocalCameraViewManager.swift +41 -0
  14. package/ios/VideoCall/OmiRemoteCameraViewBridge.m +14 -0
  15. package/ios/VideoCall/OmiRemoteCameraViewManager.swift +40 -0
  16. package/lib/commonjs/NativeOmikitPlugin.js +2 -1
  17. package/lib/commonjs/NativeOmikitPlugin.js.map +1 -1
  18. package/lib/commonjs/index.js.map +1 -1
  19. package/lib/commonjs/omi_audio_type.js +5 -7
  20. package/lib/commonjs/omi_audio_type.js.map +1 -1
  21. package/lib/commonjs/omi_call_state.js +5 -3
  22. package/lib/commonjs/omi_call_state.js.map +1 -1
  23. package/lib/commonjs/omi_local_camera.js +19 -17
  24. package/lib/commonjs/omi_local_camera.js.map +1 -1
  25. package/lib/commonjs/omi_remote_camera.js +20 -17
  26. package/lib/commonjs/omi_remote_camera.js.map +1 -1
  27. package/lib/commonjs/omi_start_call_status.js +5 -24
  28. package/lib/commonjs/omi_start_call_status.js.map +1 -1
  29. package/lib/commonjs/omikit.js +56 -3
  30. package/lib/commonjs/omikit.js.map +1 -1
  31. package/lib/commonjs/types/index.d.js.map +1 -1
  32. package/lib/module/NativeOmikitPlugin.js.map +1 -1
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/omi_audio_type.js +4 -7
  35. package/lib/module/omi_audio_type.js.map +1 -1
  36. package/lib/module/omi_call_state.js +4 -3
  37. package/lib/module/omi_call_state.js.map +1 -1
  38. package/lib/module/omi_local_camera.js +19 -18
  39. package/lib/module/omi_local_camera.js.map +1 -1
  40. package/lib/module/omi_remote_camera.js +20 -18
  41. package/lib/module/omi_remote_camera.js.map +1 -1
  42. package/lib/module/omi_start_call_status.js +4 -24
  43. package/lib/module/omi_start_call_status.js.map +1 -1
  44. package/lib/module/omikit.js +49 -1
  45. package/lib/module/omikit.js.map +1 -1
  46. package/lib/module/types/index.d.js.map +1 -1
  47. package/omikit-plugin.podspec +1 -1
  48. package/package.json +2 -11
  49. package/react-native.config.js +14 -0
  50. package/src/NativeOmikitPlugin.ts +1 -0
  51. package/src/omi_call_state.tsx +1 -0
  52. package/src/omi_local_camera.tsx +15 -19
  53. package/src/omi_remote_camera.tsx +16 -19
  54. package/src/omikit.tsx +63 -0
  55. package/src/types/index.d.ts +344 -62
  56. package/android/src/main/java/com/omikitplugin/FLLocalCameraModule.kt +0 -34
  57. package/android/src/main/java/com/omikitplugin/FLLocalCameraView.kt +0 -44
  58. package/android/src/main/java/com/omikitplugin/FLRemoteCameraModule.kt +0 -37
  59. package/android/src/main/java/com/omikitplugin/FLRemoteCameraView.kt +0 -23
  60. package/ios/VideoCall/FLLocalCameraView.m +0 -17
  61. package/ios/VideoCall/FLLocalCameraView.swift +0 -44
  62. package/ios/VideoCall/FLRemoteCameraView.m +0 -18
  63. package/ios/VideoCall/FLRemoteCameraView.swift +0 -124
@@ -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.4"
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
- val client = OmiClient.getInstance(reactApplicationContext!!)
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
- currentActivity?.runOnUiThread {
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 = currentActivity
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 = currentActivity
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 = currentActivity
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 = currentActivity
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 = currentActivity
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 = currentActivity
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 = currentActivity
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 = reactApplicationContext?.currentActivity ?: run {
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
- reactApplicationContext.currentActivity?.startActivityForResult(intent, REQUEST_OVERLAY_PERMISSION_CODE)
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 = reactApplicationContext?.currentActivity ?: run {
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 = reactApplicationContext?.currentActivity ?: return
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: FLLocalCameraView? = null
12
- private var remoteView: FLRemoteCameraView? = null
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 = FLLocalCameraView(reactContext)
16
+ localView = OmiLocalCameraView(reactContext)
16
17
  }
17
18
  if (remoteView == null) {
18
- remoteView = FLRemoteCameraView(reactContext)
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
- FLLocalCameraModule(reactContext, localView!!),
23
- FLRemoteCameraModule(reactContext, remoteView!!),
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 = FLLocalCameraView(reactContext)
32
+ localView = OmiLocalCameraView(reactContext)
30
33
  }
31
34
  if (remoteView == null) {
32
- remoteView = FLRemoteCameraView(reactContext)
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 (videoManager == nil && call.isVideo) {
389
- videoManager = OMIVideoViewManager.init()
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
- if (videoManager != nil) {
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
- //video call
600
- func toggleCamera() {
601
- if let videoManager = videoManager {
602
- videoManager.toggleCamera()
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
- func getCameraStatus() -> Bool {
607
- guard let videoManager = videoManager else { return false }
608
- return videoManager.isCameraOn
609
- }
610
-
611
- func switchCamera() {
612
- if let videoManager = videoManager {
613
- videoManager.switchCamera()
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 getLocalPreviewView(frame: CGRect) -> UIView? {
618
- guard let videoManager = videoManager else { return nil}
619
- return videoManager.createView(forVideoLocal: frame)
684
+
685
+ func toggleCamera() {
686
+ OMIVideoCallManager.shared().toggleCamera()
620
687
  }
621
-
622
- func getRemotePreviewView(frame: CGRect) -> UIView? {
623
- guard let videoManager = videoManager else { return nil }
624
- return videoManager.createView(forVideoRemote: frame)
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)