omikit-plugin 4.1.0 → 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 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.1.0
5
+ **Status:** Active maintenance | **Version:** 4.1.1
6
6
 
7
7
  ---
8
8
 
@@ -46,7 +46,7 @@ The [omikit-plugin](https://www.npmjs.com/package/omikit-plugin) enables VoIP/SI
46
46
 
47
47
  | Platform | SDK | Version |
48
48
  |----------|-----|---------|
49
- | Android | OMIKIT | 2.6.4 |
49
+ | Android | OMIKIT | 2.6.6 |
50
50
  | iOS | OmiKit | 1.11.4 |
51
51
 
52
52
  ### Platform Requirements
@@ -54,8 +54,8 @@ The [omikit-plugin](https://www.npmjs.com/package/omikit-plugin) enables VoIP/SI
54
54
  | | Android | iOS |
55
55
  |--|---------|-----|
56
56
  | **Min SDK** | API 24 (Android 7.0) | iOS 13.0 |
57
- | **Target SDK** | API 35 (Android 15) | — |
58
- | **Compile SDK** | API 35 | — |
57
+ | **Target SDK** | API 36 (Android 16) | — |
58
+ | **Compile SDK** | API 36 | — |
59
59
 
60
60
  ### Device Requirements
61
61
 
@@ -131,7 +131,32 @@ Add to `android/app/src/main/AndroidManifest.xml`:
131
131
  >
132
132
  > Make sure to add the `tools` namespace to your manifest tag: `xmlns:tools="http://schemas.android.com/tools"`
133
133
 
134
- ### 2. Firebase Cloud Messaging (FCM)
134
+ ### 2. Incoming Call Activity (Required)
135
+
136
+ Your main Activity must handle incoming call intents from the SDK. Add the following `intent-filter` to your `MainActivity` in `AndroidManifest.xml`:
137
+
138
+ ```xml
139
+ <activity
140
+ android:name=".MainActivity"
141
+ android:showWhenLocked="true"
142
+ android:turnScreenOn="true"
143
+ android:launchMode="singleTask"
144
+ ...>
145
+
146
+ <!-- Incoming call intent-filter (required for lock screen) -->
147
+ <intent-filter>
148
+ <action android:name="${applicationId}.ACTION_INCOMING_CALL" />
149
+ <action android:name="android.intent.action.CALL" />
150
+ <category android:name="android.intent.category.DEFAULT" />
151
+ <data android:host="incoming_call" android:scheme="omisdk" />
152
+ </intent-filter>
153
+ </activity>
154
+ ```
155
+
156
+ > **Important:** The `${applicationId}.ACTION_INCOMING_CALL` action ensures incoming calls show correctly on **lock screen** for Android 9-14. Without this, the default dialer may intercept the intent instead of your app.
157
+
158
+ ### 3. Firebase Cloud Messaging (FCM)
159
+
135
160
 
136
161
  Add your `google-services.json` to `android/app/`.
137
162
 
@@ -141,7 +166,7 @@ In `android/app/build.gradle`:
141
166
  apply plugin: 'com.google.gms.google-services'
142
167
  ```
143
168
 
144
- ### 3. Maven Repository
169
+ ### 4. Maven Repository
145
170
 
146
171
  **Option A — `settings.gradle.kts` (recommended for new projects)**
147
172
 
@@ -201,7 +226,7 @@ OMI_TOKEN=omi_github_access_token
201
226
 
202
227
  > **Note:** Contact the OMICall development team to get `OMI_USER` and `OMI_TOKEN` credentials.
203
228
 
204
- ### 4. New Architecture (Optional)
229
+ ### 5. New Architecture (Optional)
205
230
 
206
231
  To enable New Architecture on Android, in `android/gradle.properties`:
207
232
 
@@ -238,13 +263,352 @@ Enable **Push Notifications** capability in Xcode for VoIP push (PushKit).
238
263
 
239
264
  ### 4. AppDelegate Setup
240
265
 
241
- In your `AppDelegate.mm` (or `.m`):
266
+ The AppDelegate template differs depending on your React Native version. Choose the one that matches your project:
267
+
268
+ #### RN 0.74 – 0.78 (RCTAppDelegate pattern)
269
+
270
+ <details>
271
+ <summary>AppDelegate.h</summary>
242
272
 
243
273
  ```objc
244
- #import <OmiKit/OmiKit-umbrella.h>
245
- #import <OmiKit/Constants.h>
274
+ #import <RCTAppDelegate.h>
275
+ #import <UIKit/UIKit.h>
276
+ #import <UserNotifications/UserNotifications.h>
277
+ #import <OmiKit/OmiKit.h>
278
+
279
+ @interface AppDelegate : RCTAppDelegate <UIApplicationDelegate, RCTBridgeDelegate, UNUserNotificationCenterDelegate>
280
+
281
+ @property (nonatomic, strong) UIWindow *window;
282
+ @property (nonatomic, strong) PushKitManager *pushkitManager;
283
+ @property (nonatomic, strong) CallKitProviderDelegate *provider;
284
+ @property (nonatomic, strong) PKPushRegistry *voipRegistry;
285
+
286
+ @end
246
287
  ```
247
288
 
289
+ </details>
290
+
291
+ <details>
292
+ <summary>AppDelegate.mm</summary>
293
+
294
+ ```objc
295
+ #import "AppDelegate.h"
296
+ #import <Firebase.h>
297
+ #import <React/RCTBundleURLProvider.h>
298
+ #import <OmiKit/OmiKit.h>
299
+
300
+ #if __has_include("OmikitNotification.h")
301
+ #import "OmikitNotification.h"
302
+ #else
303
+ #import <omikit_plugin/OmikitNotification.h>
304
+ #endif
305
+
306
+ @implementation AppDelegate
307
+
308
+ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
309
+ {
310
+ self.moduleName = @"YourAppName"; // Replace with your app name
311
+
312
+ // ----- OmiKit Config ------
313
+ [OmiClient setEnviroment:KEY_OMI_APP_ENVIROMENT_SANDBOX
314
+ userNameKey:@"full_name"
315
+ maxCall:2
316
+ callKitImage:@"call_image"
317
+ typePushVoip:TYPE_PUSH_CALLKIT_DEFAULT];
318
+
319
+ self.provider = [[CallKitProviderDelegate alloc]
320
+ initWithCallManager:[OMISIPLib sharedInstance].callManager];
321
+ self.voipRegistry = [[PKPushRegistry alloc]
322
+ initWithQueue:dispatch_get_main_queue()];
323
+ self.pushkitManager = [[PushKitManager alloc]
324
+ initWithVoipRegistry:self.voipRegistry];
325
+
326
+ if (@available(iOS 10.0, *)) {
327
+ [UNUserNotificationCenter currentNotificationCenter].delegate =
328
+ (id<UNUserNotificationCenterDelegate>)self;
329
+ }
330
+
331
+ if ([FIRApp defaultApp] == nil) {
332
+ [FIRApp configure];
333
+ }
334
+ // ----- End OmiKit Config ------
335
+
336
+ return [super application:application didFinishLaunchingWithOptions:launchOptions];
337
+ }
338
+
339
+ // Handle foreground notifications
340
+ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
341
+ willPresentNotification:(UNNotification *)notification
342
+ withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
343
+ {
344
+ completionHandler(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge);
345
+ }
346
+
347
+ // Handle missed call notification tap
348
+ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
349
+ didReceiveNotificationResponse:(UNNotificationResponse *)response
350
+ withCompletionHandler:(void (^)())completionHandler
351
+ {
352
+ NSDictionary *userInfo = response.notification.request.content.userInfo;
353
+ if (userInfo && [userInfo valueForKey:@"omisdkCallerNumber"]) {
354
+ [OmikitNotification didRecieve:userInfo];
355
+ }
356
+ completionHandler();
357
+ }
358
+
359
+ // Register push notification token
360
+ - (void)application:(UIApplication *)app
361
+ didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)devToken
362
+ {
363
+ const unsigned char *data = (const unsigned char *)[devToken bytes];
364
+ NSMutableString *token = [NSMutableString string];
365
+ for (NSUInteger i = 0; i < [devToken length]; i++) {
366
+ [token appendFormat:@"%02.2hhX", data[i]];
367
+ }
368
+ [OmiClient setUserPushNotificationToken:[token copy]];
369
+ }
370
+
371
+ // Terminate all calls when app is killed
372
+ - (void)applicationWillTerminate:(UIApplication *)application {
373
+ @try {
374
+ [OmiClient OMICloseCall];
375
+ } @catch (NSException *exception) {}
376
+ }
377
+
378
+ - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
379
+ {
380
+ return [self bundleURL];
381
+ }
382
+
383
+ - (NSURL *)bundleURL
384
+ {
385
+ #if DEBUG
386
+ return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
387
+ #else
388
+ return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
389
+ #endif
390
+ }
391
+
392
+ @end
393
+ ```
394
+
395
+ </details>
396
+
397
+ #### RN 0.79+ (RCTReactNativeFactory pattern)
398
+
399
+ RN 0.79+ uses `RCTReactNativeFactory` instead of `RCTAppDelegate`. Add OmiKit setup in your existing AppDelegate:
400
+
401
+ <details>
402
+ <summary>AppDelegate.swift (Swift template)</summary>
403
+
404
+ ```swift
405
+ import UIKit
406
+ import React
407
+ import ReactAppDependencyProvider
408
+ import OmiKit
409
+
410
+ @main
411
+ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
412
+ var window: UIWindow?
413
+ var provider: CallKitProviderDelegate?
414
+ var pushkitManager: PushKitManager?
415
+ var voipRegistry: PKPushRegistry?
416
+
417
+ func application(_ application: UIApplication,
418
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
419
+ // React Native setup
420
+ let delegate = ReactNativeDelegate()
421
+ let factory = RCTReactNativeFactory(delegate: delegate)
422
+ delegate.dependencyProvider = RCTAppDependencyProvider()
423
+
424
+ window = UIWindow(frame: UIScreen.main.bounds)
425
+ factory.startReactNative(
426
+ withModuleName: "YourAppName",
427
+ in: window,
428
+ launchOptions: launchOptions
429
+ )
430
+
431
+ // ----- OmiKit Config ------
432
+ #ifdef DEBUG
433
+ OmiClient.setEnviroment(KEY_OMI_APP_ENVIROMENT_SANDBOX,
434
+ userNameKey: "full_name",
435
+ maxCall: 1,
436
+ callKitImage: "call_image",
437
+ typePushVoip: TYPE_PUSH_CALLKIT_DEFAULT)
438
+ #else
439
+ OmiClient.setEnviroment(KEY_OMI_APP_ENVIROMENT_PRODUCTION,
440
+ userNameKey: "full_name",
441
+ maxCall: 1,
442
+ callKitImage: "call_image",
443
+ typePushVoip: TYPE_PUSH_CALLKIT_DEFAULT)
444
+ #endif
445
+
446
+ provider = CallKitProviderDelegate(callManager: OMISIPLib.sharedInstance().callManager)
447
+ voipRegistry = PKPushRegistry(queue: .main)
448
+ pushkitManager = PushKitManager(voipRegistry: voipRegistry!)
449
+
450
+ UNUserNotificationCenter.current().delegate = self
451
+ FirebaseApp.configure()
452
+ // ----- End OmiKit Config ------
453
+
454
+ return true
455
+ }
456
+
457
+ // Handle missed call notification tap
458
+ func userNotificationCenter(_ center: UNUserNotificationCenter,
459
+ didReceive response: UNNotificationResponse,
460
+ withCompletionHandler completionHandler: @escaping () -> Void) {
461
+ let userInfo = response.notification.request.content.userInfo
462
+ if userInfo["omisdkCallerNumber"] != nil {
463
+ OmikitNotification.didRecieve(userInfo)
464
+ }
465
+ completionHandler()
466
+ }
467
+
468
+ // Register push notification token
469
+ func application(_ application: UIApplication,
470
+ didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
471
+ let token = deviceToken.map { String(format: "%02.2hhX", $0) }.joined()
472
+ OmiClient.setUserPushNotificationToken(token)
473
+ }
474
+
475
+ // Terminate all calls when app is killed
476
+ func applicationWillTerminate(_ application: UIApplication) {
477
+ try? OmiClient.omiCloseCall()
478
+ }
479
+ }
480
+
481
+ class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
482
+ override func sourceURL(for bridge: RCTBridge) -> URL? {
483
+ return bundleURL()
484
+ }
485
+
486
+ override func bundleURL() -> URL? {
487
+ #if DEBUG
488
+ return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
489
+ #else
490
+ return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
491
+ #endif
492
+ }
493
+ }
494
+ ```
495
+
496
+ </details>
497
+
498
+ <details>
499
+ <summary>AppDelegate.mm (Objective-C template)</summary>
500
+
501
+ ```objc
502
+ #import "AppDelegate.h"
503
+ #import <Firebase.h>
504
+ #import <React/RCTBundleURLProvider.h>
505
+ #import <React/RCTReactNativeFactory.h>
506
+ #import <ReactAppDependencyProvider/RCTAppDependencyProvider.h>
507
+ #import <OmiKit/OmiKit.h>
508
+
509
+ #if __has_include("OmikitNotification.h")
510
+ #import "OmikitNotification.h"
511
+ #else
512
+ #import <omikit_plugin/OmikitNotification.h>
513
+ #endif
514
+
515
+ @interface ReactNativeDelegate : RCTDefaultReactNativeFactoryDelegate
516
+ @end
517
+
518
+ @implementation AppDelegate
519
+
520
+ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
521
+ {
522
+ ReactNativeDelegate *delegate = [ReactNativeDelegate new];
523
+ RCTReactNativeFactory *factory = [[RCTReactNativeFactory alloc] initWithDelegate:delegate];
524
+ delegate.dependencyProvider = [RCTAppDependencyProvider new];
525
+
526
+ self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
527
+ [factory startReactNativeWithModuleName:@"YourAppName" in:self.window launchOptions:launchOptions];
528
+
529
+ // ----- OmiKit Config ------
530
+ #ifdef DEBUG
531
+ [OmiClient setEnviroment:KEY_OMI_APP_ENVIROMENT_SANDBOX userNameKey:@"full_name" maxCall:1 callKitImage:@"icYourApp" typePushVoip:@"background"];
532
+ #else
533
+ [OmiClient setEnviroment:KEY_OMI_APP_ENVIROMENT_PRODUCTION userNameKey:@"full_name" maxCall:1 callKitImage:@"icYourApp" typePushVoip:@"background"];
534
+ #endif
535
+
536
+ self.provider = [[CallKitProviderDelegate alloc]
537
+ initWithCallManager:[OMISIPLib sharedInstance].callManager];
538
+ self.voipRegistry = [[PKPushRegistry alloc]
539
+ initWithQueue:dispatch_get_main_queue()];
540
+ self.pushkitManager = [[PushKitManager alloc]
541
+ initWithVoipRegistry:self.voipRegistry];
542
+
543
+ if (@available(iOS 10.0, *)) {
544
+ [UNUserNotificationCenter currentNotificationCenter].delegate =
545
+ (id<UNUserNotificationCenterDelegate>)self;
546
+ }
547
+
548
+ if ([FIRApp defaultApp] == nil) {
549
+ [FIRApp configure];
550
+ }
551
+ // ----- End OmiKit Config ------
552
+
553
+ return YES;
554
+ }
555
+
556
+ // Handle missed call notification tap
557
+ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
558
+ didReceiveNotificationResponse:(UNNotificationResponse *)response
559
+ withCompletionHandler:(void (^)())completionHandler
560
+ {
561
+ NSDictionary *userInfo = response.notification.request.content.userInfo;
562
+ if (userInfo && [userInfo valueForKey:@"omisdkCallerNumber"]) {
563
+ [OmikitNotification didRecieve:userInfo];
564
+ }
565
+ completionHandler();
566
+ }
567
+
568
+ // Register push notification token
569
+ - (void)application:(UIApplication *)app
570
+ didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)devToken
571
+ {
572
+ const unsigned char *data = (const unsigned char *)[devToken bytes];
573
+ NSMutableString *token = [NSMutableString string];
574
+ for (NSUInteger i = 0; i < [devToken length]; i++) {
575
+ [token appendFormat:@"%02.2hhX", data[i]];
576
+ }
577
+ [OmiClient setUserPushNotificationToken:[token copy]];
578
+ }
579
+
580
+ // Terminate all calls when app is killed
581
+ - (void)applicationWillTerminate:(UIApplication *)application {
582
+ @try {
583
+ [OmiClient OMICloseCall];
584
+ } @catch (NSException *exception) {}
585
+ }
586
+
587
+ @end
588
+
589
+ @implementation ReactNativeDelegate
590
+
591
+ - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
592
+ {
593
+ return [self bundleURL];
594
+ }
595
+
596
+ - (NSURL *)bundleURL
597
+ {
598
+ #if DEBUG
599
+ return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
600
+ #else
601
+ return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
602
+ #endif
603
+ }
604
+
605
+ @end
606
+ ```
607
+
608
+ </details>
609
+
610
+ > **Note:** Replace `YourAppName` with your app's module name. For production, change `KEY_OMI_APP_ENVIROMENT_SANDBOX` to `KEY_OMI_APP_ENVIROMENT_PRODUCTION`.
611
+
248
612
  ### 5. New Architecture (Optional)
249
613
 
250
614
  In your `Podfile`:
@@ -253,15 +617,20 @@ In your `Podfile`:
253
617
  ENV['RN_NEW_ARCH_ENABLED'] = '1'
254
618
  ```
255
619
 
256
- For **full bridgeless mode**, in `AppDelegate.mm`:
620
+ For **New Architecture with video call support**, add Fabric interop registration in `AppDelegate.mm` inside `didFinishLaunchingWithOptions`, **before** `return [super ...]`:
257
621
 
258
622
  ```objc
259
- - (BOOL)bridgelessEnabled
260
- {
261
- return YES;
262
- }
623
+ // Required imports at the top of AppDelegate.mm
624
+ #import <React-RCTFabric/React/RCTComponentViewFactory.h>
625
+ #import <React-RCTFabric/React/RCTLegacyViewManagerInteropComponentView.h>
626
+
627
+ // Inside didFinishLaunchingWithOptions, before return:
628
+ [RCTLegacyViewManagerInteropComponentView supportLegacyViewManagerWithName:@"OmiLocalCameraView"];
629
+ [RCTLegacyViewManagerInteropComponentView supportLegacyViewManagerWithName:@"OmiRemoteCameraView"];
263
630
  ```
264
631
 
632
+ > **Important:** Bridgeless mode is **not yet supported** for video call views. If you use New Architecture, keep bridge mode enabled (do **not** add `bridgelessEnabled` returning `YES`).
633
+
265
634
  Then run `cd ios && pod install`.
266
635
 
267
636
  ---
@@ -293,7 +662,7 @@ Then run `cd ios && pod install`.
293
662
  │ │ │ │ │ │
294
663
  │ ▼ │ │ ▼ │
295
664
  │ OMIKIT SDK │ │ OmiKit SDK │
296
- │ (v2.6.4) │ │ (v1.10.34) │
665
+ │ (v2.6.5) │ │ (v1.11.4) │
297
666
  │ │ │ │ │ │
298
667
  │ ▼ │ │ ▼ │
299
668
  │ SIP Stack │ │ SIP Stack │
@@ -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:+"
@@ -5,7 +5,7 @@ import android.util.Log
5
5
  import android.view.Surface
6
6
  import android.view.TextureView
7
7
  import android.view.ViewGroup
8
- import android.widget.LinearLayout
8
+ import android.widget.FrameLayout
9
9
  import com.facebook.react.bridge.Promise
10
10
  import com.facebook.react.bridge.ReactApplicationContext
11
11
  import com.facebook.react.bridge.ReactMethod
@@ -17,24 +17,25 @@ import vn.vihat.omicall.omisdk.videoutils.ScaleManager
17
17
  import vn.vihat.omicall.omisdk.videoutils.Size
18
18
 
19
19
  class OmiLocalCameraView(private val context: ReactApplicationContext) :
20
- SimpleViewManager<LinearLayout>() {
20
+ SimpleViewManager<FrameLayout>() {
21
21
 
22
- val localView: LinearLayout = LinearLayout(context)
22
+ val localView: FrameLayout = FrameLayout(context)
23
23
  private val cameraView: TextureView = TextureView(context)
24
24
 
25
- // Track whether the surface is ready for rendering
26
25
  @Volatile
27
26
  private var isSurfaceReady = false
28
-
29
- // Queued refresh — executed when surface becomes available
30
27
  private var pendingRefreshPromise: Promise? = null
31
28
 
32
29
  init {
33
- localView.addView(cameraView)
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
+
34
36
  cameraView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
35
37
  override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
36
38
  isSurfaceReady = true
37
- // Execute queued refresh if any
38
39
  pendingRefreshPromise?.let { promise ->
39
40
  pendingRefreshPromise = null
40
41
  doRefresh(promise)
@@ -46,36 +47,28 @@ class OmiLocalCameraView(private val context: ReactApplicationContext) :
46
47
  override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
47
48
  isSurfaceReady = false
48
49
  pendingRefreshPromise = null
49
- return true
50
+ return false
50
51
  }
51
52
 
52
53
  override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
53
54
  }
54
55
  }
55
56
 
56
- override fun getName(): String {
57
- return "OmiLocalCameraView"
58
- }
57
+ override fun getName(): String = "OmiLocalCameraView"
59
58
 
60
- override fun createViewInstance(p0: ThemedReactContext): LinearLayout {
61
- // Detach from previous parent if remounted
62
- // (avoids "The specified child already has a parent" crash)
59
+ override fun createViewInstance(p0: ThemedReactContext): FrameLayout {
63
60
  (localView.parent as? ViewGroup)?.removeView(localView)
64
61
  return localView
65
62
  }
66
63
 
67
- fun localViewInstance(): LinearLayout {
68
- return localView
69
- }
64
+ fun localViewInstance(): FrameLayout = localView
70
65
 
71
- // Exposed to JS via NativeModules.OmiLocalCameraView.refresh()
72
66
  @ReactMethod
73
67
  fun refresh(promise: Promise) {
74
68
  UiThreadUtil.runOnUiThread {
75
69
  if (isSurfaceReady && cameraView.surfaceTexture != null) {
76
70
  doRefresh(promise)
77
71
  } else {
78
- // Surface not ready yet — queue and execute when available
79
72
  pendingRefreshPromise = promise
80
73
  }
81
74
  }
@@ -84,25 +77,14 @@ class OmiLocalCameraView(private val context: ReactApplicationContext) :
84
77
  private fun doRefresh(promise: Promise) {
85
78
  try {
86
79
  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
-
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
+ )
106
88
  promise.resolve(true)
107
89
  } catch (e: Exception) {
108
90
  Log.e("OmiLocalCameraView", "Error refreshing: ${e.message}")
@@ -5,6 +5,7 @@ import android.util.Log
5
5
  import android.view.Surface
6
6
  import android.view.TextureView
7
7
  import android.view.ViewGroup
8
+ import android.widget.FrameLayout
8
9
  import com.facebook.react.bridge.Promise
9
10
  import com.facebook.react.bridge.ReactApplicationContext
10
11
  import com.facebook.react.bridge.ReactMethod
@@ -16,22 +17,30 @@ import vn.vihat.omicall.omisdk.videoutils.ScaleManager
16
17
  import vn.vihat.omicall.omisdk.videoutils.Size
17
18
 
18
19
  class OmiRemoteCameraView(private val context: ReactApplicationContext) :
19
- SimpleViewManager<TextureView>() {
20
+ SimpleViewManager<FrameLayout>() {
20
21
 
21
- val remoteView: TextureView = TextureView(context)
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)
22
29
 
23
- // Track whether the surface is ready for rendering
24
30
  @Volatile
25
31
  private var isSurfaceReady = false
26
-
27
- // Queued refresh — executed when surface becomes available
28
32
  private var pendingRefreshPromise: Promise? = null
29
33
 
30
34
  init {
35
+ instance = this
36
+ remoteContainer.addView(remoteView, FrameLayout.LayoutParams(
37
+ FrameLayout.LayoutParams.MATCH_PARENT,
38
+ FrameLayout.LayoutParams.MATCH_PARENT
39
+ ))
40
+
31
41
  remoteView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
32
42
  override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
33
43
  isSurfaceReady = true
34
- // Execute queued refresh if any
35
44
  pendingRefreshPromise?.let { promise ->
36
45
  pendingRefreshPromise = null
37
46
  doRefresh(promise)
@@ -43,36 +52,28 @@ class OmiRemoteCameraView(private val context: ReactApplicationContext) :
43
52
  override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
44
53
  isSurfaceReady = false
45
54
  pendingRefreshPromise = null
46
- return true
55
+ return false
47
56
  }
48
57
 
49
58
  override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
50
59
  }
51
60
  }
52
61
 
53
- override fun getName(): String {
54
- return "OmiRemoteCameraView"
55
- }
62
+ override fun getName(): String = "OmiRemoteCameraView"
56
63
 
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
64
+ override fun createViewInstance(p0: ThemedReactContext): FrameLayout {
65
+ (remoteContainer.parent as? ViewGroup)?.removeView(remoteContainer)
66
+ return remoteContainer
62
67
  }
63
68
 
64
- fun remoteViewInstance(): TextureView {
65
- return remoteView
66
- }
69
+ fun remoteViewInstance(): FrameLayout = remoteContainer
67
70
 
68
- // Exposed to JS via NativeModules.OmiRemoteCameraView.refresh()
69
71
  @ReactMethod
70
72
  fun refresh(promise: Promise) {
71
73
  UiThreadUtil.runOnUiThread {
72
74
  if (isSurfaceReady && remoteView.surfaceTexture != null) {
73
75
  doRefresh(promise)
74
76
  } else {
75
- // Surface not ready yet — queue and execute when available
76
77
  pendingRefreshPromise = promise
77
78
  }
78
79
  }
@@ -80,15 +81,15 @@ class OmiRemoteCameraView(private val context: ReactApplicationContext) :
80
81
 
81
82
  private fun doRefresh(promise: Promise) {
82
83
  try {
83
- // Connect TextureView surface to SDK incoming video feed
84
84
  val surface = Surface(remoteView.surfaceTexture)
85
85
  OmiClient.getInstance(context.applicationContext).setupIncomingVideoFeed(surface)
86
86
  Log.d("OmiRemoteCameraView", "Connected remote video feed to surface")
87
87
 
88
+ // Default landscape; updated by onVideoSize when PJSIP reports actual dimensions
88
89
  ScaleManager.adjustAspectRatio(
89
90
  remoteView,
90
91
  Size(remoteView.width, remoteView.height),
91
- Size(1280, 720)
92
+ Size(640, 480)
92
93
  )
93
94
  promise.resolve(true)
94
95
  } catch (e: Exception) {
@@ -96,4 +97,21 @@ class OmiRemoteCameraView(private val context: ReactApplicationContext) :
96
97
  promise.resolve(false)
97
98
  }
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
+ }
99
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 {
@@ -633,7 +639,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
633
639
  return
634
640
  }
635
641
 
636
- currentActivity?.runOnUiThread {
642
+ safeActivity?.runOnUiThread {
637
643
  try {
638
644
  // Extract parameters from data with proper defaults
639
645
  val notificationIcon = data.getString("notificationIcon") ?: "ic_notification"
@@ -898,7 +904,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
898
904
  )
899
905
  val map: WritableMap = WritableNativeMap()
900
906
  if (audio == PackageManager.PERMISSION_GRANTED) {
901
- val activity = currentActivity
907
+ val activity = safeActivity
902
908
  if (activity == null) {
903
909
  promise.reject("E_NO_ACTIVITY", "Current activity is null")
904
910
  return
@@ -966,7 +972,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
966
972
  @ReactMethod
967
973
  fun joinCall(promise: Promise) {
968
974
  val appContext = reactApplicationContext.applicationContext
969
- val activity = currentActivity
975
+ val activity = safeActivity
970
976
 
971
977
  if (appContext == null) {
972
978
  promise.reject("E_NULL_CONTEXT", "Application context is null")
@@ -1075,7 +1081,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1075
1081
 
1076
1082
  @ReactMethod
1077
1083
  fun toggleSpeaker(promise: Promise) {
1078
- val activity = currentActivity
1084
+ val activity = safeActivity
1079
1085
  if (activity == null) { promise.resolve(null); return }
1080
1086
  activity.runOnUiThread {
1081
1087
  val newStatus = OmiClient.getInstance(reactApplicationContext!!).toggleSpeaker()
@@ -1086,7 +1092,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1086
1092
 
1087
1093
  @ReactMethod
1088
1094
  fun sendDTMF(data: ReadableMap, promise: Promise) {
1089
- val activity = currentActivity
1095
+ val activity = safeActivity
1090
1096
  if (activity == null) { promise.resolve(false); return }
1091
1097
  activity.runOnUiThread {
1092
1098
  val character = data.getString("character")
@@ -1106,7 +1112,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1106
1112
 
1107
1113
  @ReactMethod
1108
1114
  fun switchOmiCamera(promise: Promise) {
1109
- val activity = currentActivity
1115
+ val activity = safeActivity
1110
1116
  if (activity == null) { promise.resolve(false); return }
1111
1117
  activity.runOnUiThread {
1112
1118
  OmiClient.getInstance(reactApplicationContext!!).switchCamera()
@@ -1116,7 +1122,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1116
1122
 
1117
1123
  @ReactMethod
1118
1124
  fun toggleOmiVideo(promise: Promise) {
1119
- val activity = currentActivity
1125
+ val activity = safeActivity
1120
1126
  if (activity == null) { promise.resolve(false); return }
1121
1127
  activity.runOnUiThread {
1122
1128
  OmiClient.getInstance(reactApplicationContext!!).toggleCamera()
@@ -1354,7 +1360,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1354
1360
 
1355
1361
  @ReactMethod
1356
1362
  fun transferCall(data: ReadableMap, promise: Promise) {
1357
- val activity = currentActivity
1363
+ val activity = safeActivity
1358
1364
  if (activity == null) { promise.resolve(false); return }
1359
1365
  activity.runOnUiThread {
1360
1366
  val phone = data.getString("phoneNumber")
@@ -1559,7 +1565,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1559
1565
  // Store promise for callback
1560
1566
  permissionPromise = promise
1561
1567
 
1562
- val activity = reactApplicationContext?.currentActivity ?: run {
1568
+ val activity = safeActivity ?: run {
1563
1569
  promise.resolve(false)
1564
1570
  return
1565
1571
  }
@@ -1681,7 +1687,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1681
1687
  Uri.parse("package:${reactApplicationContext.packageName}")
1682
1688
  )
1683
1689
  intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
1684
- reactApplicationContext.currentActivity?.startActivityForResult(intent, REQUEST_OVERLAY_PERMISSION_CODE)
1690
+ safeActivity?.startActivityForResult(intent, REQUEST_OVERLAY_PERMISSION_CODE)
1685
1691
  } else {
1686
1692
  promise.resolve(true)
1687
1693
  }
@@ -1737,7 +1743,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1737
1743
  // Store promise for callback
1738
1744
  permissionPromise = promise
1739
1745
 
1740
- val activity = reactApplicationContext?.currentActivity ?: run {
1746
+ val activity = safeActivity ?: run {
1741
1747
  promise.reject("E_NULL_ACTIVITY", "Current activity is null")
1742
1748
  return
1743
1749
  }
@@ -1759,7 +1765,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1759
1765
  return
1760
1766
  }
1761
1767
 
1762
- val activity = reactApplicationContext?.currentActivity ?: return
1768
+ val activity = safeActivity ?: return
1763
1769
  ActivityCompat.requestPermissions(
1764
1770
  activity,
1765
1771
  missingPermissions.toTypedArray(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omikit-plugin",
3
- "version": "4.1.0",
3
+ "version": "4.1.1",
4
4
  "description": "Omikit Plugin by ViHAT",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",