omikit-plugin 4.0.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +272 -24
  2. package/android/src/main/java/com/omikitplugin/OmiLocalCameraView.kt +112 -0
  3. package/android/src/main/java/com/omikitplugin/OmiRemoteCameraView.kt +99 -0
  4. package/android/src/main/java/com/omikitplugin/OmikitPluginModule.kt +5 -4
  5. package/android/src/main/java/com/omikitplugin/OmikitPluginPackage.kt +11 -8
  6. package/ios/CallProcess/CallManager.swift +99 -29
  7. package/ios/Library/OmikitPlugin.m +18 -0
  8. package/ios/Library/OmikitPlugin.swift +233 -1
  9. package/ios/OmikitPlugin-Bridging-Header.h +1 -0
  10. package/ios/OmikitPlugin.xcodeproj/project.pbxproj +4 -4
  11. package/ios/VideoCall/OmiLocalCameraViewBridge.m +14 -0
  12. package/ios/VideoCall/OmiLocalCameraViewManager.swift +41 -0
  13. package/ios/VideoCall/OmiRemoteCameraViewBridge.m +14 -0
  14. package/ios/VideoCall/OmiRemoteCameraViewManager.swift +40 -0
  15. package/lib/commonjs/NativeOmikitPlugin.js +2 -1
  16. package/lib/commonjs/NativeOmikitPlugin.js.map +1 -1
  17. package/lib/commonjs/index.js.map +1 -1
  18. package/lib/commonjs/omi_audio_type.js +5 -7
  19. package/lib/commonjs/omi_audio_type.js.map +1 -1
  20. package/lib/commonjs/omi_call_state.js +5 -3
  21. package/lib/commonjs/omi_call_state.js.map +1 -1
  22. package/lib/commonjs/omi_local_camera.js +19 -17
  23. package/lib/commonjs/omi_local_camera.js.map +1 -1
  24. package/lib/commonjs/omi_remote_camera.js +20 -17
  25. package/lib/commonjs/omi_remote_camera.js.map +1 -1
  26. package/lib/commonjs/omi_start_call_status.js +5 -24
  27. package/lib/commonjs/omi_start_call_status.js.map +1 -1
  28. package/lib/commonjs/omikit.js +56 -3
  29. package/lib/commonjs/omikit.js.map +1 -1
  30. package/lib/commonjs/types/index.d.js.map +1 -1
  31. package/lib/module/NativeOmikitPlugin.js.map +1 -1
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/omi_audio_type.js +4 -7
  34. package/lib/module/omi_audio_type.js.map +1 -1
  35. package/lib/module/omi_call_state.js +4 -3
  36. package/lib/module/omi_call_state.js.map +1 -1
  37. package/lib/module/omi_local_camera.js +19 -18
  38. package/lib/module/omi_local_camera.js.map +1 -1
  39. package/lib/module/omi_remote_camera.js +20 -18
  40. package/lib/module/omi_remote_camera.js.map +1 -1
  41. package/lib/module/omi_start_call_status.js +4 -24
  42. package/lib/module/omi_start_call_status.js.map +1 -1
  43. package/lib/module/omikit.js +49 -1
  44. package/lib/module/omikit.js.map +1 -1
  45. package/lib/module/types/index.d.js.map +1 -1
  46. package/omikit-plugin.podspec +1 -1
  47. package/package.json +2 -11
  48. package/react-native.config.js +14 -0
  49. package/src/NativeOmikitPlugin.ts +1 -0
  50. package/src/omi_call_state.tsx +1 -0
  51. package/src/omi_local_camera.tsx +15 -19
  52. package/src/omi_remote_camera.tsx +16 -19
  53. package/src/omikit.tsx +63 -0
  54. package/src/types/index.d.ts +344 -62
  55. package/android/src/main/java/com/omikitplugin/FLLocalCameraModule.kt +0 -34
  56. package/android/src/main/java/com/omikitplugin/FLLocalCameraView.kt +0 -44
  57. package/android/src/main/java/com/omikitplugin/FLRemoteCameraModule.kt +0 -37
  58. package/android/src/main/java/com/omikitplugin/FLRemoteCameraView.kt +0 -23
  59. package/ios/VideoCall/FLLocalCameraView.m +0 -17
  60. package/ios/VideoCall/FLLocalCameraView.swift +0 -44
  61. package/ios/VideoCall/FLRemoteCameraView.m +0 -18
  62. package/ios/VideoCall/FLRemoteCameraView.swift +0 -124
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The [omikit-plugin](https://www.npmjs.com/package/omikit-plugin) enables VoIP/SIP calling via the OMICALL platform with support for both Old and **New Architecture** (TurboModules + Fabric).
4
4
 
5
- **Status:** Active maintenance | **Version:** 4.0.1
5
+ **Status:** Active maintenance | **Version:** 4.1.0
6
6
 
7
7
  ---
8
8
 
@@ -47,7 +47,36 @@ The [omikit-plugin](https://www.npmjs.com/package/omikit-plugin) enables VoIP/SI
47
47
  | Platform | SDK | Version |
48
48
  |----------|-----|---------|
49
49
  | Android | OMIKIT | 2.6.4 |
50
- | iOS | OmiKit | 1.10.34 |
50
+ | iOS | OmiKit | 1.11.4 |
51
+
52
+ ### Platform Requirements
53
+
54
+ | | Android | iOS |
55
+ |--|---------|-----|
56
+ | **Min SDK** | API 24 (Android 7.0) | iOS 13.0 |
57
+ | **Target SDK** | API 35 (Android 15) | — |
58
+ | **Compile SDK** | API 35 | — |
59
+
60
+ ### Device Requirements
61
+
62
+ | Requirement | Platform | Notes |
63
+ |-------------|----------|-------|
64
+ | **Physical device** | iOS (required) | iOS Simulator is **not supported** — OmiKit binary is arm64 device-only |
65
+ | **Physical device** | Android (recommended) | Emulator works for basic UI testing but VoIP/audio routing is unreliable |
66
+ | **Google Play Services** | Android (required) | Required for FCM push notifications |
67
+ | **Microphone** | Both (required) | Required for all calls |
68
+ | **Camera** | Both (optional) | Only required for video calls |
69
+ | **Internet** | Both (required) | SIP registration + RTP media streaming |
70
+
71
+ ### Package Size
72
+
73
+ | Component | Size |
74
+ |-----------|------|
75
+ | npm package (total) | ~353 KB |
76
+ | Android native code | ~4.7 MB |
77
+ | iOS native code | ~176 KB |
78
+
79
+ > **Note:** These sizes are for the plugin only. The native SDKs (OmiKit/OMIKIT) are installed separately via CocoaPods/Maven and will add to the final app size.
51
80
 
52
81
  ---
53
82
 
@@ -945,44 +974,259 @@ omiEmitter.addListener(OmiCallEvent.onCallStateChanged, (data) => {
945
974
 
946
975
  ## Video Calls
947
976
 
948
- ### Setup
977
+ > **Important:** To use video calls, you must login with `isVideo: true` in `initCallWithUserPassword()`. This enables camera support during SIP registration.
978
+
979
+ ### Prerequisites
949
980
 
950
981
  ```typescript
951
- import { OmiLocalCamera, OmiRemoteCamera } from 'omikit-plugin';
982
+ // Login with video support enabled
983
+ await initCallWithUserPassword({
984
+ userName: 'sip_user',
985
+ password: 'sip_password',
986
+ realm: 'your_realm',
987
+ isVideo: true, // Required for video call support
988
+ fcmToken: fcmToken,
989
+ });
952
990
  ```
953
991
 
954
992
  ### Video Components
955
993
 
956
994
  ```tsx
957
- // Local camera preview (your camera)
958
- <OmiLocalCamera style={{ width: 120, height: 160 }} />
959
-
960
- // Remote camera view (other party's video)
961
- <OmiRemoteCamera style={{ width: '100%', height: '100%' }} />
995
+ import {
996
+ OmiLocalCameraView, // Your camera preview
997
+ OmiRemoteCameraView, // Remote party's video
998
+ registerVideoEvent,
999
+ removeVideoEvent,
1000
+ refreshRemoteCamera,
1001
+ refreshLocalCamera,
1002
+ switchOmiCamera,
1003
+ toggleOmiVideo,
1004
+ setupVideoContainers, // iOS Fabric only
1005
+ setCameraConfig, // iOS Fabric only
1006
+ omiEmitter,
1007
+ OmiCallEvent,
1008
+ OmiCallState,
1009
+ } from 'omikit-plugin';
962
1010
  ```
963
1011
 
964
- ### Video Call Flow
1012
+ ### Platform Differences
965
1013
 
966
- ```typescript
967
- // 1. Register for video events BEFORE starting call
968
- await registerVideoEvent();
1014
+ | Feature | Android | iOS (Old Arch) | iOS (New Arch / Fabric) |
1015
+ |---------|---------|----------------|------------------------|
1016
+ | Video rendering | JSX components | JSX components | Native window containers |
1017
+ | Camera views | `<OmiRemoteCameraView>` in JSX | `<OmiRemoteCameraView>` in JSX | `setupVideoContainers()` from JS |
1018
+ | Style control | React `style` props | React `style` props | `setCameraConfig()` from JS |
1019
+ | Controls overlay | Overlay on video | Overlay on video | Below video (split layout) |
969
1020
 
970
- // 2. Start video call
971
- await startCall({ phoneNumber: '0901234567', isVideo: true });
1021
+ ### Cross-Platform Video Call Example
972
1022
 
973
- // 3. Toggle video during call
974
- await toggleOmiVideo(); // on/off video stream
975
- await switchOmiCamera(); // front/back camera
1023
+ Complete example supporting both Android and iOS (Old + New Architecture):
976
1024
 
977
- // 4. Listen for remote video ready
978
- omiEmitter.addListener(OmiCallEvent.onRemoteVideoReady, () => {
979
- // Remote video is now available - show OmiRemoteCamera
1025
+ ```tsx
1026
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
1027
+ import { View, Text, TouchableOpacity, StyleSheet, Platform, Dimensions } from 'react-native';
1028
+ import {
1029
+ OmiRemoteCameraView, OmiLocalCameraView,
1030
+ omiEmitter, OmiCallEvent, OmiCallState,
1031
+ registerVideoEvent, removeVideoEvent,
1032
+ refreshRemoteCamera, refreshLocalCamera,
1033
+ setupVideoContainers, setCameraConfig,
1034
+ switchOmiCamera, toggleOmiVideo, toggleMute, endCall,
1035
+ } from 'omikit-plugin';
1036
+
1037
+ const { width: SW, height: SH } = Dimensions.get('window');
1038
+
1039
+ export const VideoCallScreen = ({ navigation, route }) => {
1040
+ const [isCallActive, setIsCallActive] = useState(false);
1041
+ const [isMuted, setIsMuted] = useState(false);
1042
+ const [cameraOn, setCameraOn] = useState(true);
1043
+ const hasNavigated = useRef(false);
1044
+
1045
+ // iOS Fabric: configure native video container position/size
1046
+ const configureIOSVideo = useCallback(() => {
1047
+ setCameraConfig({
1048
+ target: 'remote',
1049
+ x: 0, y: 0, width: SW, height: SH * 0.65,
1050
+ scaleMode: 'fill', backgroundColor: '#000',
1051
+ });
1052
+ setCameraConfig({
1053
+ target: 'local',
1054
+ x: SW - 136, y: 60, width: 120, height: 180,
1055
+ borderRadius: 12, scaleMode: 'fill',
1056
+ });
1057
+ }, []);
1058
+
1059
+ useEffect(() => {
1060
+ // iOS: register video events before call
1061
+ if (Platform.OS === 'ios') registerVideoEvent();
1062
+
1063
+ const sub = omiEmitter.addListener(OmiCallEvent.onCallStateChanged, (data) => {
1064
+ const { status } = data;
1065
+
1066
+ if (status === OmiCallState.confirmed) {
1067
+ setIsCallActive(true);
1068
+
1069
+ if (Platform.OS === 'android') {
1070
+ // Android: connect video feeds to TextureView surfaces
1071
+ refreshRemoteCamera();
1072
+ refreshLocalCamera();
1073
+ } else {
1074
+ // iOS Fabric: create native window containers + connect SDK
1075
+ setupVideoContainers().then(() => configureIOSVideo());
1076
+ }
1077
+ }
1078
+
1079
+ if ((status === OmiCallState.disconnected || status === 6) && !hasNavigated.current) {
1080
+ hasNavigated.current = true;
1081
+ // iOS: hide native containers before navigating
1082
+ if (Platform.OS === 'ios') {
1083
+ setCameraConfig({ target: 'remote', hidden: true });
1084
+ setCameraConfig({ target: 'local', hidden: true });
1085
+ }
1086
+ setTimeout(() => {
1087
+ navigation.reset({ index: 0, routes: [{ name: 'Home' }] });
1088
+ }, 100);
1089
+ }
1090
+ });
1091
+
1092
+ return () => {
1093
+ sub.remove();
1094
+ if (Platform.OS === 'ios') removeVideoEvent();
1095
+ };
1096
+ }, [navigation, configureIOSVideo]);
1097
+
1098
+ const handleEndCall = useCallback(() => {
1099
+ if (hasNavigated.current) return;
1100
+ hasNavigated.current = true;
1101
+ if (Platform.OS === 'ios') {
1102
+ setCameraConfig({ target: 'remote', hidden: true });
1103
+ setCameraConfig({ target: 'local', hidden: true });
1104
+ }
1105
+ endCall();
1106
+ setTimeout(() => {
1107
+ navigation.reset({ index: 0, routes: [{ name: 'Home' }] });
1108
+ }, 1000);
1109
+ }, [navigation]);
1110
+
1111
+ return (
1112
+ <View style={styles.container}>
1113
+ {/* ============ VIDEO AREA ============ */}
1114
+
1115
+ {/* Android: camera views via JSX — controls can overlay on top */}
1116
+ {Platform.OS === 'android' && isCallActive && (
1117
+ <>
1118
+ <OmiRemoteCameraView style={StyleSheet.absoluteFillObject} />
1119
+ <OmiLocalCameraView style={styles.localPiP} />
1120
+ </>
1121
+ )}
1122
+
1123
+ {/* iOS Fabric: native video renders on window (top ~65%).
1124
+ React controls render below video area. */}
1125
+
1126
+ {/* ============ CONTROLS AREA ============ */}
1127
+
1128
+ {/* Spacer — pushes controls to bottom */}
1129
+ <View style={{ flex: 1 }} />
1130
+
1131
+ {/* Controls panel */}
1132
+ {isCallActive && (
1133
+ <View style={styles.controls}>
1134
+ <TouchableOpacity onPress={() => { toggleMute(); setIsMuted(m => !m); }}>
1135
+ <Text style={styles.btn}>{isMuted ? 'Unmute' : 'Mute'}</Text>
1136
+ </TouchableOpacity>
1137
+ <TouchableOpacity onPress={() => { toggleOmiVideo(); setCameraOn(c => !c); }}>
1138
+ <Text style={styles.btn}>{cameraOn ? 'Cam Off' : 'Cam On'}</Text>
1139
+ </TouchableOpacity>
1140
+ <TouchableOpacity onPress={switchOmiCamera}>
1141
+ <Text style={styles.btn}>Flip</Text>
1142
+ </TouchableOpacity>
1143
+ <TouchableOpacity onPress={handleEndCall}>
1144
+ <Text style={[styles.btn, { backgroundColor: 'red' }]}>End</Text>
1145
+ </TouchableOpacity>
1146
+ </View>
1147
+ )}
1148
+
1149
+ {/* Answer/Reject for incoming calls */}
1150
+ {!isCallActive && (
1151
+ <View style={styles.controls}>
1152
+ <TouchableOpacity onPress={handleEndCall}>
1153
+ <Text style={[styles.btn, { backgroundColor: 'red' }]}>Reject</Text>
1154
+ </TouchableOpacity>
1155
+ </View>
1156
+ )}
1157
+ </View>
1158
+ );
1159
+ };
1160
+
1161
+ const styles = StyleSheet.create({
1162
+ container: { flex: 1, backgroundColor: '#1E3050' },
1163
+ localPiP: {
1164
+ position: 'absolute', top: 60, right: 16,
1165
+ width: 120, height: 180, borderRadius: 12, overflow: 'hidden', zIndex: 10,
1166
+ },
1167
+ controls: {
1168
+ flexDirection: 'row', justifyContent: 'space-evenly',
1169
+ paddingVertical: 20, paddingHorizontal: 16,
1170
+ backgroundColor: 'rgba(0,0,0,0.5)', borderTopLeftRadius: 24, borderTopRightRadius: 24,
1171
+ paddingBottom: 40,
1172
+ },
1173
+ btn: {
1174
+ color: '#fff', fontSize: 14, paddingVertical: 12, paddingHorizontal: 16,
1175
+ backgroundColor: 'rgba(255,255,255,0.2)', borderRadius: 24, overflow: 'hidden',
1176
+ },
980
1177
  });
1178
+ ```
981
1179
 
982
- // 5. Cleanup when call ends
983
- await removeVideoEvent();
1180
+ > **Key differences per platform:**
1181
+ > - **Android**: `<OmiRemoteCameraView>` + `<OmiLocalCameraView>` render in JSX. Call `refreshRemoteCamera()` + `refreshLocalCamera()` when confirmed. Controls overlay on video.
1182
+ > - **iOS (Old Arch)**: Same as Android — JSX camera views work normally.
1183
+ > - **iOS (New Arch/Fabric)**: `RCTViewManager.view()` not called. Use `setupVideoContainers()` to create native window containers, then `setCameraConfig()` to adjust position/style. Controls render below video (split layout).
1184
+ > - **Disconnect**: On iOS, call `setCameraConfig({ target: 'remote', hidden: true })` before navigating to prevent stale video overlay. Use `setTimeout` for navigation — native event callback may block immediate navigation.
1185
+ > - **End call**: Use `navigation.reset()` instead of `goBack()` to clear stacked screens. Guard with `useRef` to prevent multiple navigations.
1186
+
1187
+ ### `setCameraConfig()` — iOS Fabric Only
1188
+
1189
+ Control native video container style from JS:
1190
+
1191
+ ```typescript
1192
+ setCameraConfig({
1193
+ target: 'local' | 'remote',
1194
+ x?: number, // X position
1195
+ y?: number, // Y position
1196
+ width?: number, // View width
1197
+ height?: number, // View height
1198
+ borderRadius?: number, // Corner radius
1199
+ borderWidth?: number, // Border width
1200
+ borderColor?: string, // Hex color (#RRGGBB or #RRGGBBAA)
1201
+ backgroundColor?: string,
1202
+ opacity?: number, // 0.0 - 1.0
1203
+ hidden?: boolean, // Show/hide
1204
+ scaleMode?: 'fill' | 'fit' | 'stretch',
1205
+ });
984
1206
  ```
985
1207
 
1208
+ ### Video API Reference
1209
+
1210
+ | Function | Description | Platform |
1211
+ |----------|-------------|----------|
1212
+ | `registerVideoEvent()` | Register for video notifications | iOS only |
1213
+ | `removeVideoEvent()` | Cleanup video notifications | iOS only |
1214
+ | `refreshRemoteCamera()` | Connect remote video feed to surface | Both |
1215
+ | `refreshLocalCamera()` | Connect local camera feed to surface | Both |
1216
+ | `switchOmiCamera()` | Switch front/back camera | Both |
1217
+ | `toggleOmiVideo()` | Toggle camera on/off during call | Both |
1218
+ | `setupVideoContainers()` | Create native video containers on window | iOS Fabric only |
1219
+ | `setCameraConfig()` | Control video container style/position | iOS Fabric only |
1220
+
1221
+ ### Known Limitations
1222
+
1223
+ - **iOS Fabric**: Controls cannot overlay on top of video (native window z-order). Use split layout — video top, controls bottom.
1224
+ - **iOS Fabric**: Camera switch has ~3-6s delay (SDK Metal stabilization).
1225
+ - **Android**: Must login with `isVideo: true` for camera to initialize. Without it, colorbar test pattern shows instead of camera.
1226
+ - **Simulator**: Video calls require physical device on both platforms.
1227
+
1228
+ See [Known Issues](./docs/known-issues.md) for full details.
1229
+
986
1230
  ---
987
1231
 
988
1232
  ## Push Notifications
@@ -1214,7 +1458,10 @@ if (initialCall) {
1214
1458
  | Incoming call on Android — no UI | Missing overlay permission | Call `requestSystemAlertWindowPermission()` |
1215
1459
  | `startCall` returns 450/451/452 | Missing runtime permissions | Call `requestPermissionsByCodes([code])` |
1216
1460
  | No audio during call | Audio route issue | Check `getAudio()` and `setAudio()` |
1217
- | Video not showing | Video event not registered | Call `registerVideoEvent()` before the call |
1461
+ | Video not showing (iOS) | Video event not registered | Call `registerVideoEvent()` before the call |
1462
+ | Video shows colorbar (Android) | Login with `isVideo: false` | Login with `isVideo: true` to enable camera |
1463
+ | Video black screen (Android) | Feed not connected | Ensure `refreshRemoteCamera()` + `refreshLocalCamera()` called on confirmed |
1464
+ | iOS Fabric: controls hidden | Native video on UIWindow covers React | Use split layout — video top, controls bottom. See [Known Issues](./docs/known-issues.md) |
1218
1465
  | NativeEventEmitter warning (iOS) | Old RN bridge issue | Upgrade to v4.0.x with New Architecture |
1219
1466
  | `Invalid local URI` in logs | Empty proxy/host in login | Pass `host` parameter in `initCallWithUserPassword` |
1220
1467
  | Build error with New Arch | Codegen not configured | Ensure `codegenConfig` exists in `package.json` |
@@ -1227,6 +1474,7 @@ if (initialCall) {
1227
1474
  Full documentation in [`./docs/`](./docs/):
1228
1475
 
1229
1476
  - [API Integration Guide (App-to-App)](./docs/api-integration-guide.md)
1477
+ - [Known Issues & Limitations](./docs/known-issues.md)
1230
1478
  - [Project Overview & PDR](./docs/project-overview-pdr.md)
1231
1479
  - [Codebase Summary](./docs/codebase-summary.md)
1232
1480
  - [System Architecture](./docs/system-architecture.md)
@@ -0,0 +1,112 @@
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.LinearLayout
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<LinearLayout>() {
21
+
22
+ val localView: LinearLayout = LinearLayout(context)
23
+ private val cameraView: TextureView = TextureView(context)
24
+
25
+ // Track whether the surface is ready for rendering
26
+ @Volatile
27
+ private var isSurfaceReady = false
28
+
29
+ // Queued refresh — executed when surface becomes available
30
+ private var pendingRefreshPromise: Promise? = null
31
+
32
+ init {
33
+ localView.addView(cameraView)
34
+ cameraView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
35
+ override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
36
+ isSurfaceReady = true
37
+ // Execute queued refresh if any
38
+ pendingRefreshPromise?.let { promise ->
39
+ pendingRefreshPromise = null
40
+ doRefresh(promise)
41
+ }
42
+ }
43
+
44
+ override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {}
45
+
46
+ override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
47
+ isSurfaceReady = false
48
+ pendingRefreshPromise = null
49
+ return true
50
+ }
51
+
52
+ override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
53
+ }
54
+ }
55
+
56
+ override fun getName(): String {
57
+ return "OmiLocalCameraView"
58
+ }
59
+
60
+ override fun createViewInstance(p0: ThemedReactContext): LinearLayout {
61
+ // Detach from previous parent if remounted
62
+ // (avoids "The specified child already has a parent" crash)
63
+ (localView.parent as? ViewGroup)?.removeView(localView)
64
+ return localView
65
+ }
66
+
67
+ fun localViewInstance(): LinearLayout {
68
+ return localView
69
+ }
70
+
71
+ // Exposed to JS via NativeModules.OmiLocalCameraView.refresh()
72
+ @ReactMethod
73
+ fun refresh(promise: Promise) {
74
+ UiThreadUtil.runOnUiThread {
75
+ if (isSurfaceReady && cameraView.surfaceTexture != null) {
76
+ doRefresh(promise)
77
+ } else {
78
+ // Surface not ready yet — queue and execute when available
79
+ pendingRefreshPromise = promise
80
+ }
81
+ }
82
+ }
83
+
84
+ private fun doRefresh(promise: Promise) {
85
+ try {
86
+ val surface = Surface(cameraView.surfaceTexture)
87
+ val client = OmiClient.getInstance(context.applicationContext)
88
+
89
+ // Connect local camera feed — delay slightly to ensure camera subsystem ready
90
+ // (matches native SDK sample behavior with AppUtils.postDelay)
91
+ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
92
+ try {
93
+ client.setupLocalVideoFeed(surface)
94
+ Log.d("OmiLocalCameraView", "Connected local video feed to surface")
95
+
96
+ ScaleManager.adjustAspectRatio(
97
+ cameraView,
98
+ Size(cameraView.width, cameraView.height),
99
+ Size(9, 16)
100
+ )
101
+ } catch (e: Exception) {
102
+ Log.e("OmiLocalCameraView", "Error setting up local feed: ${e.message}")
103
+ }
104
+ }, 300)
105
+
106
+ promise.resolve(true)
107
+ } catch (e: Exception) {
108
+ Log.e("OmiLocalCameraView", "Error refreshing: ${e.message}")
109
+ promise.resolve(false)
110
+ }
111
+ }
112
+ }
@@ -0,0 +1,99 @@
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 com.facebook.react.bridge.Promise
9
+ import com.facebook.react.bridge.ReactApplicationContext
10
+ import com.facebook.react.bridge.ReactMethod
11
+ import com.facebook.react.bridge.UiThreadUtil
12
+ import com.facebook.react.uimanager.SimpleViewManager
13
+ import com.facebook.react.uimanager.ThemedReactContext
14
+ import vn.vihat.omicall.omisdk.OmiClient
15
+ import vn.vihat.omicall.omisdk.videoutils.ScaleManager
16
+ import vn.vihat.omicall.omisdk.videoutils.Size
17
+
18
+ class OmiRemoteCameraView(private val context: ReactApplicationContext) :
19
+ SimpleViewManager<TextureView>() {
20
+
21
+ val remoteView: TextureView = TextureView(context)
22
+
23
+ // Track whether the surface is ready for rendering
24
+ @Volatile
25
+ private var isSurfaceReady = false
26
+
27
+ // Queued refresh — executed when surface becomes available
28
+ private var pendingRefreshPromise: Promise? = null
29
+
30
+ init {
31
+ remoteView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
32
+ override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
33
+ isSurfaceReady = true
34
+ // Execute queued refresh if any
35
+ pendingRefreshPromise?.let { promise ->
36
+ pendingRefreshPromise = null
37
+ doRefresh(promise)
38
+ }
39
+ }
40
+
41
+ override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {}
42
+
43
+ override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
44
+ isSurfaceReady = false
45
+ pendingRefreshPromise = null
46
+ return true
47
+ }
48
+
49
+ override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
50
+ }
51
+ }
52
+
53
+ override fun getName(): String {
54
+ return "OmiRemoteCameraView"
55
+ }
56
+
57
+ override fun createViewInstance(p0: ThemedReactContext): TextureView {
58
+ // Detach from previous parent if remounted
59
+ // (avoids "The specified child already has a parent" crash)
60
+ (remoteView.parent as? ViewGroup)?.removeView(remoteView)
61
+ return remoteView
62
+ }
63
+
64
+ fun remoteViewInstance(): TextureView {
65
+ return remoteView
66
+ }
67
+
68
+ // Exposed to JS via NativeModules.OmiRemoteCameraView.refresh()
69
+ @ReactMethod
70
+ fun refresh(promise: Promise) {
71
+ UiThreadUtil.runOnUiThread {
72
+ if (isSurfaceReady && remoteView.surfaceTexture != null) {
73
+ doRefresh(promise)
74
+ } else {
75
+ // Surface not ready yet — queue and execute when available
76
+ pendingRefreshPromise = promise
77
+ }
78
+ }
79
+ }
80
+
81
+ private fun doRefresh(promise: Promise) {
82
+ try {
83
+ // Connect TextureView surface to SDK incoming video feed
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
+ ScaleManager.adjustAspectRatio(
89
+ remoteView,
90
+ Size(remoteView.width, remoteView.height),
91
+ Size(1280, 720)
92
+ )
93
+ promise.resolve(true)
94
+ } catch (e: Exception) {
95
+ Log.e("OmiRemoteCameraView", "Error refreshing: ${e.message}")
96
+ promise.resolve(false)
97
+ }
98
+ }
99
+ }
@@ -450,7 +450,9 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
450
450
  moduleInstance = this
451
451
  reactApplicationContext!!.addActivityEventListener(this)
452
452
  Handler(Looper.getMainLooper()).post {
453
- val client = OmiClient.getInstance(reactApplicationContext!!)
453
+ // Use applicationContext — pjsip video subsystem needs it for CameraManager access
454
+ val ctx = reactApplicationContext?.applicationContext ?: reactApplicationContext!!
455
+ val client = OmiClient.getInstance(ctx)
454
456
  client.addCallStateListener(this)
455
457
  client.addCallStateListener(autoUnregisterListener)
456
458
  client.setDebug(false)
@@ -463,10 +465,9 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
463
465
  try {
464
466
  // ✅ Prepare audio system trước khi start services
465
467
  prepareAudioSystem()
466
-
468
+
467
469
  OmiClient.getInstance(reactApplicationContext!!).addAccountListener(accountListener)
468
-
469
- // ✅ Start services - không cần prevent auto-unregister với Silent API
470
+
470
471
  OmiClient.getInstance(reactApplicationContext!!).setDebug(false)
471
472
  promise.resolve(true)
472
473
  } catch (e: Exception) {
@@ -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
  }