Skip to main content

omicall_flutter_plugin 3.4.0 copy "omicall_flutter_plugin: ^3.4.0" to clipboard
omicall_flutter_plugin: ^3.4.0 copied to clipboard

OmiKit Flutter is wrapper SDK for OmiCall for Application easy integrate features of Omicall Like Call App2App/App2Phone, Video Call.

πŸ“¦ OMICALL SDK FOR Flutter #

The OmiKit exposes the πŸ“¦ omicall_flutter_plugin library.

The most important part of the framework is :

  • βœ… Help to easy integrate with Omicall.
  • βœ… Easy custom Call UI/UX.
  • βœ… Optimize codec voip for you.
  • βœ… Full interface to interactive with core function like sound/ringtone/codec.
  • βœ… Built-in Call Quality Monitoring (MOS score tracking)

πŸ“¦ SDK Versions #

Component Version
omicall_flutter_plugin 3.4.0
Android OmiSDK (io.omicrm.vihat:omi-sdk) 2.9.1
iOS OmiKit 1.12.1 (CocoaPods or Swift Package Manager)
Android minimum minSdk 24, compileSdk 35
iOS minimum 13.0

πŸ“ Status #

Currently active maintenance and improve performance


πŸ“š Table of Contents #


Quick Start #

Install via pubspec.yaml:

dependencies:
  omicall_flutter_plugin: ^3.4.0

Minimum setup:

// 1. Start services
await OmicallClient.instance.startServices();

// 2. Login
final login = await OmicallClient.instance.register(
  userName: "your_username",
  password: "your_password",
  realm: "your_realm",
  fcmToken: fcmToken,      // FCM token (Android) / APNS token (iOS)
  host: "",                // optional SIP proxy; empty = OMICall default
  isVideo: false,
  projectId: "your_firebase_project_id",
);
if (!login.isSuccess) return; // login.message: MISSING_PARAMS | INIT_FAILED | NETWORK_UNAVAILABLE

// 3. Make a call β€” returns a JSON string {"status", "_id", "message"}
final result = await OmicallClient.instance.startCall(phoneNumber, false);

Architecture Overview #

πŸ“ System Architecture #

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        Flutter Layer                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  Your Flutter App (UI/UX)                                  β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚  β”‚
β”‚  β”‚  β”‚ Call Screen β”‚  β”‚ Dial Screen  β”‚  β”‚ Settings Screen  β”‚   β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚            β”‚                 β”‚                    β”‚              β”‚
β”‚            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚
β”‚                              β”‚                                   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                    OmicallClient API                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  β€’ startCall()        β€’ toggleAudio()   β€’ getCurrentUser() β”‚  β”‚
β”‚  β”‚  β€’ endCall()          β€’ toggleSpeaker() β€’ getGuestUser()   β”‚  β”‚
β”‚  β”‚  β€’ joinCall()         β€’ toggleHold()    β€’ getUserInfo()    β”‚  β”‚
β”‚  β”‚  β€’ sendDTMF()         β€’ toggleVideo()   β€’ logout()         β”‚  β”‚
β”‚  β”‚  β€’ switchCamera()     β€’ transferCall()                     β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                              β”‚                                   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚              Event Listeners & Helpers                     β”‚  β”‚
β”‚  β”‚  β€’ callStateChangeEvent  β€’ CallQualityTracker              β”‚  β”‚
β”‚  β”‚  β€’ setCallQualityListener β€’ CallQualityInfo                β”‚  β”‚
β”‚  β”‚  β€’ setMuteListener       β€’ VideoController                 β”‚  β”‚
β”‚  β”‚  β€’ setSpeakerListener    β€’ CameraView                      β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚ Method Channel
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   Flutter Plugin Bridge                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  Platform: Android (Kotlin)    Platform: iOS (Swift/ObjC)  β”‚  β”‚
β”‚  β”‚  β€’ OmicallsdkPlugin            β€’ SwiftOmikitPlugin         β”‚  β”‚
β”‚  β”‚  β€’ Event Broadcasting          β€’ Event Broadcasting        β”‚  β”‚
β”‚  β”‚  β€’ Permission Handling         β€’ CallKit Integration       β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Native SDK Layer                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  Android SDK (vn.vihat.omicall.omisdk)                     β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚  β”‚ SipService   β”‚  β”‚ CallManager  β”‚  β”‚ AudioManager     β”‚  β”‚  β”‚
β”‚  β”‚  β”‚ (OMISIP)      β”‚  β”‚              β”‚  β”‚                  β”‚  β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  iOS SDK (OmiKit)                                          β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚  β”‚ OMISIPLib    β”‚  β”‚ CallManager  β”‚  β”‚ PushKitManager   β”‚  β”‚  β”‚
β”‚  β”‚  β”‚ (OMISIP)      β”‚  β”‚              β”‚  β”‚                  β”‚  β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚   Network Layer     β”‚
                    β”‚  β€’ SIP Protocol     β”‚
                    β”‚  β€’ RTP/SRTP         β”‚
                    β”‚  β€’ STUN/TURN        β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ”„ Data Flow #

User Action β†’ Flutter UI β†’ OmicallClient β†’ Platform Channel
           β†’ Native SDK β†’ SIP Server β†’ Remote Peer

Remote Peer β†’ Native SDK β†’ Event Broadcast β†’ OmicallClient
           β†’ Flutter Listeners β†’ UI Update

Call Flow Lifecycle #

πŸ“ž Outgoing Call Flow #

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Flutter β”‚ startCall(phoneNumber, isVideo)
β”‚   App   β”‚ ─────────────────────────────────────────┐
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                          β”‚
                                                     β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Call State Changes                        β”‚
β”‚                                                              β”‚
β”‚  UNKNOWN (0)                                                 β”‚
β”‚      β”‚                                                       β”‚
β”‚      β”œβ”€β”€β”€ Check permissions & account status                 β”‚
β”‚      β”‚                                                       β”‚
β”‚      β–Ό                                                       β”‚
β”‚  CALLING (1) ◄─ startCallSuccess (status=8)                  β”‚
β”‚      β”‚           "Initiating call..."                        β”‚
β”‚      β”‚           ⏱️  Ring tone starts                        β”‚
β”‚      β”‚                                                       β”‚
β”‚      β–Ό                                                       β”‚
β”‚  EARLY (3)                                                   β”‚
β”‚      β”‚           "Ringing..."                                β”‚
β”‚      β”‚           ⏱️  Waiting for remote peer                 β”‚
β”‚      β”‚                                                       β”‚
β”‚      β–Ό                                                       β”‚
β”‚  CONNECTING (4)                                              β”‚
β”‚      β”‚           "Connecting..."                             β”‚
β”‚      β”‚           πŸ”Š Remote peer answered                     β”‚
β”‚      β”‚                                                       β”‚
β”‚      β–Ό                                                       β”‚
β”‚  CONFIRMED (5)                                               β”‚
β”‚      β”‚           "Active call"                               β”‚
β”‚      β”‚           πŸ“Š Call quality monitoring starts           β”‚
β”‚      β”‚           ⏱️  Call timer starts                       β”‚
β”‚      β”‚           πŸŽ™οΈ  Audio/Video stream active               β”‚
β”‚      β”‚                                                       β”‚
β”‚      β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                 β”‚
β”‚      β”‚  β”‚  User can perform:               β”‚                 β”‚
β”‚      β”‚  β”‚  β€’ toggleAudio() - Mute/Unmute   β”‚                 β”‚
β”‚      β”‚  β”‚  β€’ toggleSpeaker() - Speaker on  β”‚                 β”‚
β”‚      β”‚  β”‚  β€’ toggleHold() - Hold/Unhold    β”‚                 β”‚
β”‚      β”‚  β”‚  β€’ toggleVideo() - Video on/off  β”‚                 β”‚
β”‚      β”‚  β”‚  β€’ sendDTMF() - Send numbers     β”‚                 β”‚
β”‚      β”‚  β”‚  β€’ transferCall() - Transfer     β”‚                 β”‚
β”‚      β”‚  β”‚  β€’ endCall() - Hang up           β”‚                 β”‚
β”‚      β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                 β”‚
β”‚      β”‚                                                       β”‚
β”‚      β–Ό                                                       β”‚
β”‚  DISCONNECTED (6)                                            β”‚
β”‚      β”‚           "Call ended"                                β”‚
β”‚      β”‚           πŸ“Š Call info returned                       β”‚
β”‚      β”‚           ⏱️  Final duration calculated               β”‚
β”‚      β”‚           🧹 Cleanup resources                        β”‚
β”‚      β”‚                                                       β”‚
β”‚      β–Ό                                                       β”‚
β”‚  UNKNOWN (0)                                                 β”‚
β”‚                                                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Timeline Example:
─────────────────────────────────────────────────────────────►
0s        2s        4s        7s        45s       47s
β”‚         β”‚         β”‚         β”‚         β”‚         β”‚
UNKNOWN   CALLING   EARLY     CONNECTING CONFIRMED DISCONNECTED
          "Dialing" "Ringing" "Answered" "Talking" "Ended"

πŸ“² Incoming Call Flow #

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Push Notif  β”‚ Firebase/APNS Push
β”‚ or CallKit  β”‚ ───────────────────────────────────────┐
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                        β”‚
                                                       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Call State Changes                        β”‚
β”‚                                                              β”‚
β”‚  UNKNOWN (0)                                                 β”‚
β”‚      β”‚                                                       β”‚
β”‚      β–Ό                                                       β”‚
β”‚  INCOMING (2)                                                β”‚
β”‚      β”‚           "Incoming call from XXX"                    β”‚
β”‚      β”‚           πŸ”” Ringtone plays                           β”‚
β”‚      β”‚           πŸ“± CallKit/Notification shows               β”‚
β”‚      β”‚                                                       β”‚
β”‚      β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                 β”‚
β”‚      β”‚  β”‚  User Actions:                   β”‚                 β”‚
β”‚      β”‚  β”‚  β€’ joinCall() ────┐              β”‚                 β”‚
β”‚      β”‚  β”‚  β€’ endCall() ─────┼────┐         β”‚                 β”‚
β”‚      β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                 β”‚
β”‚      β”‚                      β”‚    β”‚                           β”‚
β”‚      β”‚  β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚                           β”‚
β”‚      β–Ό                            β”‚                          β”‚
β”‚  CONNECTING (4)                   β”‚                          β”‚
β”‚      β”‚           "Answering..."  β”‚                           β”‚
β”‚      β”‚           πŸ”Š Audio setup   β”‚                          β”‚
β”‚      β”‚                            β”‚                          β”‚
β”‚      β–Ό                            β”‚                          β”‚
β”‚  CONFIRMED (5)                    β”‚                          β”‚
β”‚      β”‚           "Active call"   β”‚                           β”‚
β”‚      β”‚           πŸ“Š Quality monitoring                       β”‚
β”‚      β”‚           πŸŽ™οΈ  Audio stream active                     β”‚
β”‚      β”‚                            β”‚                          β”‚
β”‚      β”‚  [Same actions as         β”‚                           β”‚
β”‚      β”‚   outgoing call]           β”‚                          β”‚
β”‚      β”‚                            β”‚                          β”‚
β”‚      β–Ό                            β–Ό                          β”‚
β”‚  DISCONNECTED (6) ◄───────── DISCONNECTED (6)                β”‚
β”‚      β”‚           "Answered & Ended" "Rejected"               β”‚
β”‚      β”‚           πŸ“Š Call duration    πŸ“Š No duration          β”‚
β”‚      β”‚                                                       β”‚
β”‚      β–Ό                                                       β”‚
β”‚  UNKNOWN (0)                                                 β”‚
β”‚                                                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Timeline Example (Answer):
─────────────────────────────────────────────────────────────►
0s        1s        3s        30s       32s
β”‚         β”‚         β”‚         β”‚         β”‚
UNKNOWN   INCOMING  CONNECTING CONFIRMED DISCONNECTED
          "Ringing" "Answered" "Talking" "Ended"

Timeline Example (Reject):
─────────────────────────────────────────────────────────────►
0s        1s        4s
β”‚         β”‚         β”‚
UNKNOWN   INCOMING  DISCONNECTED
          "Ringing" "Rejected"

πŸ” Hold State Flow #

CONFIRMED (5)
     β”‚
     β”œβ”€ toggleHold() ──►  setHoldListener(true)            (HOLD event)
     β”‚                    setCallHoldChangedListener(...)  (CALL_HOLD_CHANGED, per call)
     β”‚                       β”‚
     β”‚                       β”œβ”€ "Call on hold" β€” the customer hears nothing
     β”‚                       β”‚
     β”‚  ◄──── toggleHold() ───
     β–Ό
setHoldListener(false) β€” "Call resumed"

Hold is reported through the HOLD / CALL_HOLD_CHANGED listeners. CALL_STATE_CHANGED with status 7 is not emitted on Android, so do not rely on it. toggleHold() is ignored while a consult transfer is running (the SDK keeps the customer on hold).


Configuration #

πŸ› οΈ STEP 1: Config native file #

πŸš€ Android:

πŸ“Œ - Config gradle file
  • The Android SDK is served from GitHub Packages and needs credentials (ask the OMI team for the token). Put them in android/local.properties β€” never commit this file:
omicallUsername=omicall
omicallPassword=YOUR_GITHUB_PAT
// Load local.properties for OMICall SDK credentials
val localProperties = java.util.Properties()
val localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
    localPropertiesFile.inputStream().use { localProperties.load(it) }
}

allprojects {
    repositories {
        google()
        mavenCentral()
        maven {
            url = uri("https://maven.pkg.github.com/omicall/OMICall-SDK")
            credentials {
                username = localProperties.getProperty("omicallUsername")
                    ?: project.findProperty("omicallUsername") as? String ?: "omicall"
                password = localProperties.getProperty("omicallPassword")
                    ?: project.findProperty("omicallPassword") as? String ?: ""
            }
            authentication {
                create<BasicAuthentication>("basic")
            }
        }
    }
}
  • Groovy android/build.gradle equivalent:
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withInputStream { localProperties.load(it) }
}

allprojects {
    repositories {
        google()
        mavenCentral()
        maven {
            url "https://maven.pkg.github.com/omicall/OMICall-SDK"
            credentials {
                username = localProperties.getProperty('omicallUsername') ?: 'omicall'
                password = localProperties.getProperty('omicallPassword') ?: ''
            }
            authentication {
                basic(BasicAuthentication)
            }
        }
    }
}
plugins {
    id("com.android.application")
    id("kotlin-android")
    id("dev.flutter.flutter-gradle-plugin")
    id("com.google.gms.google-services") version "4.4.2"
}

Groovy: apply plugin: 'com.google.gms.google-services' plus classpath 'com.google.gms:google-services:4.4.2' in the root buildscript { dependencies { } }.

A 401 Unauthorized while resolving io.omicrm.vihat:omi-sdk means the token is missing or expired.

πŸ“Œ - Config AndroidManifest.xml file

⚠️ IMPORTANT: This configuration is required for Android 13+ (API 33+) and Android 14+ (API 34+). Missing permissions will cause crashes or prevent calls from working.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- Hardware features - telephony is optional for VoIP apps -->
    <uses-feature
        android:name="android.hardware.telephony"
        android:required="false" />

    <!-- Basic permissions -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

    <!-- Android 14+ (API 34+) - REQUIRED for foreground service types -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />

    <!-- Android 13+ (API 33+) - REQUIRED for notifications -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

    <!-- CRITICAL: Explicitly remove FOREGROUND_SERVICE_CAMERA to prevent crashes on Android 14-15 -->
    <!-- See CHANGELOG 3.3.2: this permission causes crashes on devices without camera -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" tools:node="remove" />

    <!-- Connection Service for Android 15-16 -->
    <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />

    <application
        android:name="${applicationName}"
        android:label="Your App Name"
        android:enableOnBackInvokedCallback="true"
        android:alwaysRetainTaskState="true"
        android:largeHeap="true"
        android:exported="true"
        android:supportsRtl="true"
        android:allowBackup="false"
        android:fullBackupContent="false"
        android:icon="@mipmap/ic_launcher">

        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:showWhenLocked="true"
            android:turnScreenOn="true"
            android:windowSoftInputMode="adjustResize"
            android:showOnLockScreen="true"
            android:launchMode="singleTask"
            android:largeHeap="true"
            android:alwaysRetainTaskState="true"
            android:supportsPictureInPicture="true"
            android:resizeableActivity="true"
            android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">
            <!-- supportsPictureInPicture / resizeableActivity / configChanges: only needed for
                 video Picture-in-Picture (see "Picture-in-Picture"); use false if you do not use PiP -->

            <!-- Your theme configuration -->
            <meta-data
                android:name="io.flutter.embedding.android.NormalTheme"
                android:resource="@style/NormalTheme" />

            <!-- Main launcher intent -->
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <!-- Incoming call intent -->
            <intent-filter>
                <action android:name="android.intent.action.CALL" />
                <category android:name="android.intent.category.DEFAULT" />
                <data
                    android:host="incoming_call"
                    android:scheme="omisdk" />
            </intent-filter>

        </activity>

        <!-- Firebase Message Receiver -->
        <receiver
            android:name="vn.vihat.omicall.omisdk.receiver.FirebaseMessageReceiver"
            android:exported="true"
            android:enabled="true"
            tools:replace="android:exported"
            android:permission="com.google.android.c2dm.permission.SEND">
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
            </intent-filter>
        </receiver>

        <!-- Notification Service -->
        <service
            android:name="vn.vihat.omicall.omisdk.service.NotificationService"
            android:enabled="true"
            android:exported="false"
            android:foregroundServiceType="microphone|phoneCall">
            <!-- IMPORTANT: Only microphone|phoneCall, NO camera (see CHANGELOG 3.3.2) -->
        </service>

        <!-- Flutter plugin metadata -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />

    </application>
</manifest>

πŸ“ Critical Notes:

  1. FOREGROUND_SERVICE_CAMERA removal is REQUIRED:

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" tools:node="remove" />
    
    • Without tools:node="remove", app WILL CRASH on Android 14-15 devices without camera
    • See CHANGELOG 3.3.2 for details
  2. Android 14+ requires foregroundServiceType:

    • NotificationService: android:foregroundServiceType="microphone|phoneCall" (NOT camera)
    • (foregroundServiceType has no effect on a <receiver>; older guides that put remoteMessaging on FirebaseMessageReceiver can drop it)
  3. Android 13+ requires POST_NOTIFICATIONS:

    • Request this permission at runtime for notifications to work
    • Add to your permissions request flow
  4. Android 15-16 requires MANAGE_OWN_CALLS:

    • For connection service integration
  5. Optional β€” Disable saving calls to device call history (WRITE_CALL_LOG): By default the SDK may request WRITE_CALL_LOG to log calls in the native call history. If your app does not want calls saved to the device's call history, explicitly remove this permission:

    <uses-permission android:name="android.permission.WRITE_CALL_LOG" tools:node="remove" />
    

    Note: xmlns:tools="http://schemas.android.com/tools" must be declared in the <manifest> tag (already included in the manifest template above).

Runtime Permission Request Example:

// In your MainActivity or permissions handler
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    requestPermissions(arrayOf(
        Manifest.permission.POST_NOTIFICATIONS
    ), REQUEST_CODE_NOTIFICATIONS)
}
πŸ“Œ - Config MainActivity file
  • In the MainActivity.kt file we need you to add the following configurations
import androidx.core.app.ActivityCompat.requestPermissions
import android.app.Activity
import io.flutter.embedding.android.FlutterActivity
import vn.vihat.omicall.omicallsdk.OmicallsdkPlugin
import android.Manifest
import androidx.activity.result.contract.ActivityResultContracts
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
import android.os.Bundle
import android.content.Intent
import android.content.res.Configuration
import android.util.Log
import vn.vihat.omicall.omisdk.utils.SipServiceConstants

class MainActivity: FlutterActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        try {
            val callPermissions = arrayOf(Manifest.permission.RECORD_AUDIO)

            if(!isGrantedPermission(Manifest.permission.RECORD_AUDIO)){
                requestPermissions(this,callPermissions,0)
            }

            val isIncomingCall = intent.getBooleanExtra(SipServiceConstants.ACTION_IS_INCOMING_CALL, false)
            OmicallsdkPlugin.onOmiIntent(this, intent)

        } catch (e: Throwable) {
            e.printStackTrace()
        }
    }

    override fun onNewIntent(intent: Intent){
        super.onNewIntent(intent);
        OmicallsdkPlugin.onOmiIntent(this, intent)
    }


    override fun onDestroy() {
        super.onDestroy()
        OmicallsdkPlugin.onDestroy()
    }

    override fun onResume(){
        super.onResume()
        OmicallsdkPlugin.onResume(this);
    }

    fun isGrantedPermission(permission: String): Boolean {
        return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        OmicallsdkPlugin.onRequestPermissionsResult(requestCode, permissions, grantResults, this)
    }

    // Only for video Picture-in-Picture (see "Picture-in-Picture")
    override fun onUserLeaveHint() {
        super.onUserLeaveHint()
        OmicallsdkPlugin.onUserLeaveHint(this)
    }

    override fun onPictureInPictureModeChanged(isInPip: Boolean, newConfig: Configuration) {
        super.onPictureInPictureModeChanged(isInPip, newConfig)
        OmicallsdkPlugin.onPictureInPictureModeChanged(isInPip)
    }

  // Your config
}

  • ✨ Setup push notification : Only support Firebase for remote push notification.
    • βœ… Add google-service.json in android/app (For more information, you can refer firebase_core)

    • βœ… Add Fire Messaging to receive fcm_token (You can refer firebase_messaging to setup notification for Flutter)

    • βœ… For more setting information, please refer Config Push for Android


Now let's continue configuring iOS, let's go πŸš€

πŸš€ iOS: CocoaPods or Swift Package Manager

The plugin supports both. CocoaPods stays the default; with Swift Package Manager enabled (Flutter's upcoming default) Flutter takes ios/omicall_flutter_plugin/Package.swift, which pulls OmiKit from https://github.com/VIHATTeam/OmiKit.git (1.12.x).

# your app's pubspec.yaml β€” enable SPM for this project only
flutter:
  config:
    enable-swift-package-manager: true

or globally: flutter config --enable-swift-package-manager.

With SPM, the Objective-C omicall_flutter_plugin-Swift.h header does not exist. In an Objective-C AppDelegate.m import the plugin like this (works with both):

#if __has_include(<omicall_flutter_plugin/omicall_flutter_plugin-Swift.h>)
#import <omicall_flutter_plugin/omicall_flutter_plugin-Swift.h>  // CocoaPods
#else
@import omicall_flutter_plugin;  // Swift Package Manager
#endif

A Swift AppDelegate keeps import omicall_flutter_plugin / import OmiKit in both cases. The setup below (AppDelegate, Info.plist, capabilities) is the same for both.

πŸš€ iOS(Object-C):

  • πŸ“ Assets: Add call_image into assets folder to update callkit image. We only support png style (This will help show your application icon on iOS CallKit when a call comes in)



  • πŸ“Œ Add instance variables in AppDelegate.h (ivars, not file-scope globals β€” globals in a header cause duplicate-symbol link errors):
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
#import <UserNotifications/UserNotifications.h>
#import <OmiKit/OmiKit-umbrella.h>
#import <OmiKit/Constants.h>

@interface AppDelegate : FlutterAppDelegate<UIApplicationDelegate> {
    PushKitManager *pushkitManager;
    CallKitProviderDelegate *provider;
    PKPushRegistry *voipRegistry;
}
@end



  • πŸ“Œ Edit AppDelegate.m:
#import <OmiKit/OmiKit.h>
#if __has_include(<omicall_flutter_plugin/omicall_flutter_plugin-Swift.h>)
#import <omicall_flutter_plugin/omicall_flutter_plugin-Swift.h>  // CocoaPods
#else
@import omicall_flutter_plugin;  // Swift Package Manager
#endif



  • πŸ“Œ Add these lines into didFinishLaunchingWithOptions:
[OmiClient setEnviroment:KEY_OMI_APP_ENVIROMENT_SANDBOX userNameKey:@"extension" maxCall:1 callKitImage: @"call_image" typePushVoip:@"default" representName:@"OMICALL"];
provider = [[CallKitProviderDelegate alloc] initWithCallManager: [OMISIPLib sharedInstance].callManager];
voipRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
pushkitManager = [[PushKitManager alloc] initWithVoipRegistry:voipRegistry];
if (@available(iOS 10.0, *)) {
    [UNUserNotificationCenter currentNotificationCenter].delegate = (id<UNUserNotificationCenterDelegate>) self;
}

πŸ“ Notes:

  • To custom callkit image, you need add image into assets and paste image name into setEnviroment function.
  • The variable representName is not required. If it has a value, when a call comes in, by default this name will be displayed on callKit. If nothing is transmitted, internal calls will display the Employee's name or the employee's internal

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
    bool value = [SwiftOmikitPlugin processUserActivityWithUserActivity:userActivity];
    return value;
}
// This action is used to close ongoing calls when the user kills the app
- (void)applicationWillTerminate:(UIApplication *)application {
    @try {
        [OmiClient OMICloseCall];
    }
    @catch (NSException *exception) {

    }
}
  • πŸ“Œ Add these lines into Info.plist:
<key>NSCameraUsageDescription</key>
<string>Need camera access for video call functions</string>
<key>NSMicrophoneUsageDescription</key>
<string>Need microphone access for make Call</string>
<!-- VoIP push, call audio in background (also required for video Picture-in-Picture) -->
<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
    <string>fetch</string>
    <string>remote-notification</string>
    <string>voip</string>
</array>
<!-- Calls started from the iOS Recents list (setCallLogListener) -->
<key>NSUserActivityTypes</key>
<array>
    <string>INStartCallIntent</string>
</array>
  • πŸ“Œ In Xcode β†’ Signing & Capabilities add Push Notifications and Background Modes (Audio, Voice over IP, Remote notifications).

  • πŸ’‘ Save token for OmiClient: if you added firebase_messaging in your project so you don't need add these lines.

- (void)application:(UIApplication*)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)devToken
{
    // parse token bytes to string
    const char *data = [devToken bytes];
    NSMutableString *token = [NSMutableString string];
    for (NSUInteger i = 0; i < [devToken length]; i++)
    {
        [token appendFormat:@"%02.2hhX", data[i]];
    }

    // print the token in the console.
    NSLog(@"Push Notification Token: %@", [token copy]);
    [OmiClient setUserPushNotificationToken:[token copy]];
}

✨ Push on iOS

  • βœ… Incoming calls arrive through APNS VoIP (PushKit, handled by PushKitManager) and CallKit.
  • βœ… Pass the APNS token as fcmToken to register() / registerWithApiKey() (await FirebaseMessaging.instance.getAPNSToken() when you use firebase_messaging).
  • βœ… Add GoogleService-Info.plist only if your app uses Firebase on iOS.
  • βœ… For more setting information, please refer Config Push for iOS

πŸš€ iOS(Swift):

  • πŸ“ Notes: The configurations are similar to those for object C above, with only a slight difference in the syntax of the funcs

  • πŸ“Œ Add variables in Appdelegate.swift:

import OmiKit
import PushKit
import NotificationCenter
import omicall_flutter_plugin

var pushkitManager: PushKitManager?
var provider: CallKitProviderDelegate?
var voipRegistry: PKPushRegistry?
  • πŸ“Œ Add these lines into didFinishLaunchingWithOptions:
OmiClient.setEnviroment(KEY_OMI_APP_ENVIROMENT_SANDBOX, userNameKey: "extension", maxCall: 1, callKitImage: "call_image", typePushVoip: "default", representName: "OMICALL")
provider = CallKitProviderDelegate.init(callManager: OMISIPLib.sharedInstance().callManager)
voipRegistry = PKPushRegistry.init(queue: .main)
pushkitManager = PushKitManager.init(voipRegistry: voipRegistry)

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    return SwiftOmikitPlugin.processUserActivity(userActivity: userActivity)
}

// Close ongoing calls when the user kills the app
override func applicationWillTerminate(_ application: UIApplication) {
    OmiClient.omiCloseCall()
}
  • πŸ“Œ Info.plist and capabilities: same as the Objective-C section above.

  • πŸ’‘ Save token for OmiClient: if you added firebase_messaging in your project so you don't need add these lines.

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let deviceTokenString = deviceToken.hexString
    OmiClient.setUserPushNotificationToken(deviceTokenString)
}

extension Data {
    var hexString: String {
        let hexString = map { String(format: "%02.2hhx", $0) }.joined()
        return hexString
    }
}

✨ Push on iOS

  • βœ… Incoming calls arrive through APNS VoIP (PushKit, handled by PushKitManager) and CallKit.
  • βœ… Pass the APNS token as fcmToken to register() / registerWithApiKey() (await FirebaseMessaging.instance.getAPNSToken() when you use firebase_messaging).
  • βœ… Add GoogleService-Info.plist only if your app uses Firebase on iOS.
  • βœ… For more setting information, please refer Config Push for iOS



❌ Important release note

We support 2 environments. So you need set correct key in Appdelegate.
- KEY_OMI_APP_ENVIROMENT_SANDBOX support on debug mode
- KEY_OMI_APP_ENVIROMENT_PRODUCTION support on release mode
- Visit on web admin to select correct enviroment.



πŸ› οΈ STEP 2: Integrate into Flutter code πŸš€ #

Request permission

  • πŸ“Œ Request these permissions before making a call (e.g. with permission_handler):
 + Permission.microphone
 + Permission.camera        (video calls only)
 + Permission.notification  (Android 13+, incoming-call notification)

On Android, register() also requests RECORD_AUDIO, POST_NOTIFICATIONS and β€” when isVideo: true β€” CAMERA. Pass isVideo: false for audio-only apps.

  • πŸ“Œ Set up for Firebase:
await Firebase.initializeApp();
// If you only use Firebase on Android. Add these line `if (Platform.isAndroid)`
// Because we use APNS to push notification on iOS so you don't need add Firebase for iOS.
  • πŸ“Œ Important function.

  • πŸ“ Start Serivce: OmiKit need start services and register some events.

  // Call in the root widget
  OmicallClient.instance.startServices();



πŸ’‘ You need to log in to OMI's switchboard system, we provide you with 2 functions with 2 different functions:

πŸ“ Notes: The information below is taken from the API, you should connect with our Technical team for support

  • βœ… register: for employees. They can call any telecommunications number allowed in your business on the OMI system.
String? token = await FirebaseMessaging.instance.getToken();
if (Platform.isIOS) {
    token = await FirebaseMessaging.instance.getAPNSToken();
}

final result = await OmicallClient.instance.register(
  userName: "100",
  password: "******",
  realm: "your_realm",
  fcmToken: token ?? "",
  host: "",          // optional SIP proxy; empty = OMICall default
  isVideo: true,
  projectId: "",     // optional Firebase project id
);

if (!result.isSuccess) {
  // result.message: NETWORK_UNAVAILABLE | MISSING_PARAMS | INIT_FAILED
  showError(result.message);
  return;
}
// Login successful β€” proceed

  • βœ… registerWithApiKey: usually for your customers, who only call a fixed number (for example your hotline).
final result = await OmicallClient.instance.registerWithApiKey(
  fullName: "John",
  userUuid: "user-uuid",
  apiKey: "your_api_key",
  phone: "0901234567",
  fcmToken: token ?? "",
  isVideo: true,
);
if (!result.isSuccess) showError(result.message);

initCallWithUserPassword / initCallWithApiKey still work but are deprecated since 3.4.0 β€” they return the raw JSON string. See Migration Guide.

  • βœ… Get the ongoing call when the app is opened (e.g. from a call notification / killed state):
final result = await OmicallClient.instance.getInitialCall();
// A Map with the call info when a call exists;
// otherwise `false` (iOS) or null (Android).
final hasCall = result is Map && result.isNotEmpty;
  • βœ… Config push notification:

    OmicallClient.instance.configPushNotification(
      notificationIcon: "calling_face",        // Android: notification icon (res/drawable)
      prefix: "Cuα»™c gọi tα»›i tα»«: ",             // Android: incoming-call text prefix
      userImage: "calling_face",               // Android: default avatar (res/drawable)
      prefixMissedCallMessage: "Cuα»™c gọi nhα»‘ tα»«", // both: missed-call message prefix
      missedCallTitle: "Cuα»™c gọi nhα»‘",         // iOS: missed-call title
      userNameKey: "uuid",                     // Android: uuid | full_name | extension
      channelId: "channelid.callnotification", // Android: used as <channelId>-inbound / -missed
      representName: "",                       // Android: name shown for incoming calls (optional)
      isUseIntentFilter: true,                 // Android: open the app through the intent filter
    );
    
    Key Android iOS
    notificationIcon, prefix, userImage, userNameKey, channelId, representName, isUseIntentFilter βœ… β€” (iOS takes userNameKey / representName from setEnviroment in AppDelegate)
    prefixMissedCallMessage βœ… βœ… (only when missedCallTitle is also set)
    missedCallTitle β€” βœ…
    incomingBackgroundColor, incomingAcceptButtonImage, incomingDeclineButtonImage, backImage, audioNotificationDescription, videoNotificationDescription accepted, not used by the current SDK β€”

API Reference #

Core Functions #

πŸ“Œ Authentication

register

Login for employees (can call any number allowed in business)

final OmiRegisterResult result = await OmicallClient.instance.register(
  userName: String,   // required
  password: String,   // required
  realm: String,      // required
  fcmToken: String,   // required β€” FCM token (Android) / APNS token (iOS)
  host: String,       // optional, default "" = OMICall default SIP proxy
  isVideo: bool,      // optional, default true
  projectId: String,  // optional, default ""
);
registerWithApiKey

Login for customers (call a fixed number, e.g., hotline)

final OmiRegisterResult result = await OmicallClient.instance.registerWithApiKey(
  fullName: String,   // required
  userUuid: String,   // required
  apiKey: String,     // required
  phone: String,      // required
  fcmToken: String,   // required
  isVideo: bool,      // optional, default true
  projectId: String,  // optional, default ""
);

Returns Future<OmiRegisterResult> β€” same on Android and iOS:

statusCode message Meaning
200 INIT_SUCCESS Login successful (result.isSuccess == true)
400 MISSING_PARAMS Required fields missing
500 INIT_FAILED Wrong credentials or SDK error
600 NETWORK_UNAVAILABLE No internet connection

iOS: the SIP stack registers asynchronously, so register returns 200 once the credentials are set; a wrong password shows up as a failed call, not as 500.

initCallWithUserPassword / initCallWithApiKey (deprecated)

Same as register / registerWithApiKey but return the raw JSON string {"status": Int, "message": String}.

logout

Logout current user

OmicallClient.instance.logout();
Device info & backend registration (3.4.0)

Same names and behaviour as the React Native SDK β€” use them to check or add this device on the OMI backend yourself:

final client = OmicallClient.instance;
final deviceId = await client.getDeviceId();   // value the SDK sends when adding the device
final appId = await client.getAppId();         // bundle id / package name
final fcm = await client.getFcmToken();        // push token registered by the SDK
final voip = await client.getVoipToken();      // iOS PushKit token, null on Android
final projectId = await client.getProjectId(); // Firebase project id
final sip = await client.getSipInfo();         // current SIP account

final devices = await client.getOmiDevices();  // List<OmiDeviceInfo> on the backend (HTTP, do not poll)
final boundSip = client.findSipNumberByDeviceId(devices, deviceId);
if (await client.needsReLogin()) {
  // logged in locally but the backend has no record of this device β†’ logout + register again
}
Function Returns
getDeviceId / getAppId / getFcmToken / getVoipToken / getProjectId / getSipInfo String?
getOmiDevices List<OmiDeviceInfo> (empty when not logged in or on network error)
isCurrentDeviceRegistered / needsReLogin bool (false when not logged in)
findSipNumberByDeviceId(devices, deviceId) String? (sync helper)

Call Control #

πŸ“Œ Start Call (Phone Number)

Initiate outgoing call to any number

final result = await OmicallClient.instance.startCall(
    phone,      // phone number
    _isVideoCall // if true, it's a video call; otherwise, it's an audio call.
);

Returns a JSON string {"status": int, "_id": String, "message": String} on both platforms:

final json = jsonDecode(result as String) as Map;
final started = json['status'] == 8 || json['message'] == 'START_CALL_SUCCESS';
if (started) openCallScreen(); else showError(json['message']);

The status codes differ by platform β€” branch on message for cross-platform handling.

message iOS status Android status
START_CALL_SUCCESS ⬅️ navigate to the call screen 8 (also 12 = waiting for endpoint) 8
SWITCHBOARD_REGISTERING (call continues once registered β€” treat as started) β€” 8
INVALID_UUID 0 401
INVALID_PHONE_NUMBER 1 402
SAME_PHONE_NUMBER_WITH_PHONE_REGISTER / CAN_NOT_CALL_YOURSELF 2 403
MAX_RETRY 3 β€”
PERMISSION_DENIED 4 405 (audio) / 406 (video)
COULD_NOT_FIND_END_POINT (log in first) 5 β€”
SWITCHBOARD_NOT_CONNECTED β€” 404
REGISTER_ACCOUNT_FAIL 6 β€”
START_CALL_FAIL 7 β€”
HAVE_ANOTHER_CALL 9 400
ACCOUNT_TURN_OFF_NUMBER_INTERNAL 10 β€”
NO_NETWORK 11 408

OmiStartCallStatus (iOS codes 0-11) lives in package:omicall_flutter_plugin/action/action_model.dart.

πŸ“Œ Start Call (UUID)

Call using user UUID (API key only)

final int status = await OmicallClient.instance.startCallWithUUID(
    uuid,         // user id
    _isVideoCall  // call video or audio
);
// iOS: 8 = started (codes as in the table above). Android: raw SDK code, 200 = started.
// 4 = microphone permission denied (both).
πŸ“Œ Join Call

Answer incoming call

OmicallClient.instance.joinCall();
πŸ“Œ End Call

Hang up current call

final Map? callInfo = await OmicallClient.instance.endCall();
// Declines a ringing incoming call, hangs up otherwise.
// Returns the call info (null when declining on Android, {} when there is no call on iOS).

/* Sample output:
{
  "transaction_id": "ea7dff38-cb1e-483d-8576-xxxxxxxxxxxx",
  "direction": "inbound",
  "source_number": 111,
  "destination_number": 110,
  "time_start_to_answer": 1682858097393,
  "time_end": 1682858152181,   // iOS: seconds
  "sip_user": 111,
  "disposition": "answered",
  "code_end_call": 200
}
*/
πŸ“Œ Reject / Drop Incoming Call
// 486 Busy Here β€” other devices of the same extension keep ringing
await OmicallClient.instance.rejectCall();

// 603 Decline β€” stops ringing on every device of the extension
await OmicallClient.instance.dropCall();

Both hang up the call instead when it is already answered.

πŸ“Œ Toggle Audio (Mute/Unmute)

Toggle microphone on/off

OmicallClient.instance.toggleAudio();
πŸ“Œ Toggle Speaker

Switch between earpiece and speaker

OmicallClient.instance.toggleSpeaker();
πŸ“Œ Toggle Hold

Hold/Unhold current call

OmicallClient.instance.toggleHold();
πŸ“Œ Send DTMF

Send a key (0-9, *, #)

OmicallClient.instance.sendDTMF(value);
πŸ“Œ Transfer Call

Forward call to another employee

final bool sent = await OmicallClient.instance.transferCall(phoneNumber: "101");

The PBX result arrives in setTransferStatusListener (see below). Since 3.4.0 (Android SDK 2.9.1) a failed transfer no longer ends the call on Android: you get TRANSFER_STATUS with statusCode: 500, isFinal: true and the call stays up.

πŸ“Œ Consult (Attended) Transfer β€” NEW 3.4.0

Talk to a colleague before handing the customer over (answered audio calls only).

final client = OmicallClient.instance;

client.setConsultStateListener((e) {
  // e.state: dialing β†’ ringing β†’ answered β†’ ended; e.statusCode: SIP code
  if (e.state == OmiConsultState.answered) showCompleteButton();
  if (e.state == OmiConsultState.ended) hideConsultUi();
});
client.setTransferStatusListener((e) {
  if (e.isFinal) e.isSuccess ? closeCallScreen() : showError("Transfer failed (${e.statusCode})");
});

// 1. Hold the customer and call extension 102
final status = await client.startConsultCall("102");
if (status != OmiConsultStartStatus.success) showError(status.name);

// 2a. Colleague answered β†’ hand the customer over
await client.completeConsultTransfer();

// 2b. …or go back to the customer (taken off hold)
await client.cancelConsult();
Function Returns Description
startConsultCall(extension, {extraHeaders}) OmiConsultStartStatus Hold the customer and call extension
completeConsultTransfer({extraHeaders}) bool Hang up the consult, transfer the customer to the same extension. false if not answered yet / customer already ended
cancelConsult() bool End the consult, take the customer off hold
isConsulting() bool A consult is running
isConsultOriginalEnded() bool The customer hung up while the consult is running
getActiveCalls() List<OmiCallSummary> Live calls with hold state; consult call has isConsult == true

OmiConsultStartStatus: success (200), emptyExtension (402), canNotCallYourself (403), switchboardNotConnected (404), missingAudioPermission (405), noActiveCall (409), consultInProgress (410), videoCallNotSupported (411), sameAsCustomer (412), unknown (-1, a code this plugin version does not know).

Rules (same on Android and iOS):

  • The consult call never fires CALL_STATE_CHANGED; follow it with setConsultStateListener.
  • endCall() during a consult hangs up the customer; use cancelConsult() to cancel.
  • Colleague rejects / does not answer β†’ the consult ends and the customer is taken off hold automatically.
  • Transfer fails or has no result within 30 s β†’ the customer is taken off hold.
  • toggleHold() is ignored while consulting; mute / DTMF go to the consult call once answered.
πŸ“Œ Get Current User

Retrieve logged-in user information

final user = await OmicallClient.instance.getCurrentUser();
// Output: { "extension": "111", "full_name": "John", "avatar_url": "", "uuid": "122aaa" }
πŸ“Œ Get Guest User

Retrieve remote user information

final user = await OmicallClient.instance.getGuestUser();
// Output: { "extension": "111", "full_name": "Jane", "avatar_url": "", "uuid": "456bbb" }
πŸ“Œ Get User Info (SIP)

Lookup user by phone number

final user = await OmicallClient.instance.getUserInfo(phone: "111");
// Output: { "extension": "111", "full_name": "Alice", "avatar_url": "", "uuid": "789ccc" }
πŸ“Œ Audio Output (earpiece / speaker / Bluetooth / headset)
final outputs = await OmicallClient.instance.getOutputAudios();  // List of {name, type}
final current = await OmicallClient.instance.getCurrentAudio();  // List with the active output
await OmicallClient.instance.setOutputAudio(portType: outputs.first["type"]);
OmicallClient.instance.setAudioChangedListener((List<dynamic> audios) {
  // AUDIO_CHANGE: the active output changed (Bluetooth connected, headset unplugged, ...)
});

portType is the type value from getOutputAudios(): an int on Android, a String on iOS.

πŸ“Œ Other
Function Platform Description
changeTransport(type: "UDP" | "TCP") Android Switch the SIP transport
getHistoryCallLog() iOS Number of a call started from the iOS Recents list (see Call Log Listener)
dispose() both Close the call-state stream (only when the app no longer uses the SDK)
remove…Listener() both Each set…Listener has a matching remove…Listener

Video Call Functions πŸš€πŸš€ #

πŸ“ Note: These functions support video calls only. Make sure you enable video in the initialization functions and when starting a call.

πŸ“Œ Switch Camera

Toggle between front/back camera

OmicallClient.instance.switchCamera();
πŸ“Œ Toggle Video

Turn video on/off during call

OmicallClient.instance.toggleVideo();
πŸ“Œ Remote Video Ready

setVideoListener receives REMOTE_VIDEO_READY when the first remote frame is decoded (Android and iOS since 3.4.0). registerVideoEvent() / removeVideoEvent() are kept for compatibility and are no longer needed.

πŸ“Œ How video is rendered (3.4.0)

Same as the OMICall store apps:

  • Android: each widget attaches its feed as soon as its surface exists and again when the call connects; the picture is fitted 9:16 and the remote ratio follows the size the peer really sends.
  • iOS: the widgets are containers for OMIVideoCallManager, which handles rendering, background/foreground, re-INVITE recovery, PiP and the "reconnecting" overlay. Video starts as soon as both widgets are laid out; it does not wait for CONFIRMED.
  • Show LocalCameraView / RemoteCameraView only after the call is answered (or for an outgoing call): on iOS the camera starts as soon as both widgets are on screen during a video call.
  • Do not mirror the local preview yourself (Transform.scale(scaleX: -1)): the SDK already mirrors the front camera.
πŸ“Œ Local Camera Widget

Display your camera view

LocalCameraView(
  width: double.infinity,
  height: double.infinity,
  onCameraCreated: (controller) {
    _localController = controller;
  },
)
πŸ“Œ Remote Camera Widget

Display remote camera view

RemoteCameraView(
  width: double.infinity,
  height: double.infinity,
  onCameraCreated: (controller) {
    _remoteController = controller;
  },
)
πŸ“Œ Refresh Camera

Optional since 3.4.0 (views attach themselves). Safe to call at any time:

_remoteController?.refresh();
_localController?.refresh();
πŸ“Œ Picture-in-Picture (3.4.0)

The remote video moves to a floating window when the app goes to the background during a video call; the SDK closes it at call end. Enable it once, before the call:

final supported = await OmicallClient.instance.setVideoPictureInPictureEnabled(true);
OmicallClient.instance.setPictureInPictureListener((isActive) {
  setState(() => _hideControls = isActive); // hide buttons inside the PiP window
});

Android β€” AndroidManifest.xml (MainActivity):

<activity android:name=".MainActivity"
    android:supportsPictureInPicture="true"
    android:resizeableActivity="true"
    android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" ... />

MainActivity.kt:

override fun onUserLeaveHint() {
    super.onUserLeaveHint()
    OmicallsdkPlugin.onUserLeaveHint(this)
}

override fun onPictureInPictureModeChanged(isInPip: Boolean, newConfig: Configuration) {
    super.onPictureInPictureModeChanged(isInPip, newConfig)
    OmicallsdkPlugin.onPictureInPictureModeChanged(isInPip)
}

iOS 15+: audio in UIBackgroundModes. To keep sending your camera while in PiP (iOS 16+), add the entitlement com.apple.developer.avfoundation.multitasking-camera-access (explain it in App Review notes); without it the other party sees your video paused while audio continues.

πŸ“Œ Remote Connection State (iOS, 3.4.0)

The SDK draws a "reconnecting / weak connection" overlay on the remote video. Translate it, or follow the state yourself:

OmicallClient.instance.setRemoteConnectionOverlayTexts({
  OmiRemoteConnectionState.reconnecting: "Reconnecting…",
  OmiRemoteConnectionState.videoPaused: "Video paused",
});
OmicallClient.instance.setRemoteConnectionListener((e) {
  // e.state: connected | videoPaused | weak | reconnecting | localReconnecting | networkSwitching | uplinkPoor
});

πŸ“Š Call Quality Monitoring (NEW) #

The SDK provides built-in call quality tracking using MOS (Mean Opinion Score) and LCN (Loss Connect Number) metrics.

Quick Setup

import 'package:omicall_flutter_plugin/omicall.dart';
import 'package:omicall_flutter_plugin/models/call_quality_info.dart';
import 'package:omicall_flutter_plugin/utils/call_quality_tracker.dart';

class MyCallScreen extends StatefulWidget {
  @override
  State<MyCallScreen> createState() => _MyCallScreenState();
}

class _MyCallScreenState extends State<MyCallScreen> {
  final CallQualityTracker _qualityTracker = CallQualityTracker();
  String callQuality = "";

  @override
  void initState() {
    super.initState();

    // Set up call quality listener
    OmicallClient.instance.setCallQualityListener((data) {
      // Parse call quality data using helper
      final info = _qualityTracker.parseCallQuality(data);

      debugPrint("CallQualityInfo => $info");

      // Handle loading indicator (network issue detection)
      if (info.shouldShowLoading) {
        EasyLoading.show(); // Show loading when network stuck
      } else if (info.isNetworkRecovered || info.lcn == 0) {
        EasyLoading.dismiss(); // Dismiss when network recovers
      }

      // Display MOS score
      setState(() {
        callQuality = info.mosDisplay; // "4.5", "3.2", etc.
      });
    });
  }

  @override
  void dispose() {
    _qualityTracker.reset(); // Reset tracker when screen closes
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: [
            Text("Call Quality: $callQuality"),
            // Display quality level: "Excellent", "Good", "Fair", "Poor", "Bad"
          ],
        ),
      ),
    );
  }
}

CallQualityInfo Properties

Property Type Description
mos double MOS score (1.0-5.0) - call quality metric
mosDisplay String Formatted MOS for display (e.g., "4.5")
qualityText String Quality level: "Excellent", "Good", "Fair", "Poor", "Bad"
lcn int Loss Connect Number (connection loss tracking)
quality int Quality level (0=good, 1=normal, 2=bad)
jitter double Jitter in milliseconds
latency double Latency in milliseconds
packetLoss double Packet loss percentage
shouldShowLoading bool Whether to show loading indicator
isNetworkRecovered bool Whether network has recovered
consecutiveSameLcnCount int Current consecutive same LCN count
requestCount int Request counter (req) from the native quality report

MOS Score Scale

MOS Range Quality Level Description
β‰₯ 4.0 Excellent XuαΊ₯t sαΊ―c - Perfect call quality
3.5-4.0 Good Tα»‘t - High quality
3.0-3.5 Fair ChαΊ₯p nhαΊ­n được - Acceptable
2.0-3.0 Poor KΓ©m - Low quality
< 2.0 Bad RαΊ₯t kΓ©m - Very poor

Loading Logic (Network Issue Detection)

The loading indicator is automatically shown/hidden based on LCN tracking. Android only β€” iOS always reports lcn: 0 / isNeedLoading: false, so shouldShowLoading never turns on there (use setRemoteConnectionListener on iOS instead).

Show Loading:
  β”œβ”€ The same non-zero LCN value arrives 4 times in a row (3 repeats)
  └─ Indicates network stuck/frozen

Hide Loading:
  β”œβ”€ LCN value changes (network recovered)
  └─ LCN value is 0 (no connection loss)

Timeline Example:

Event 1: LCN=5 β†’ Count=0 β†’ No loading
Event 2: LCN=5 β†’ Count=1 β†’ No loading
Event 3: LCN=5 β†’ Count=2 β†’ No loading
Event 4: LCN=5 β†’ Count=3 β†’ ⚠️ SHOW LOADING (network stuck!)
Event 5: LCN=6 β†’ Count=0 β†’ βœ… HIDE LOADING (network recovered!)

Benefits

βœ… Clean Code: No manual parsing logic in UI code βœ… Consistent: Same logic across all screens βœ… Maintainable: Update logic in one place βœ… Type Safe: Strongly typed data βœ… Testable: Easy to unit test βœ… Automatic: Loading logic handled automatically

For more details, see lib/utils/README.md


Event Listener ✨ #

πŸ“Œ Call State Change Event (IMPORTANT) #

Listen to all call state changes:

OmicallClient.instance.callStateChangeEvent.listen((action) {
  debugPrint("Action: ${action.actionName}, Data: ${action.data}");
});

Action Names

action.actionName is a string; filter on it β€” the stream carries more than call states:

actionName Constant Payload
"CALL_STATE_CHANGED" OmiEventList.onCallStateChanged see Event Data below
"SWITCHBOARD_ANSWER" OmiEventList.onSwitchboardAnswer {sip} β€” switchboard answered
"VIDEO" OmiEventList.onVideo iOS: camera on/off after toggleVideo()
"CAMERA_STATUS" β€” Android: sent after switchCamera()
OmicallClient.instance.callStateChangeEvent.listen((action) {
  if (action.actionName != OmiEventList.onCallStateChanged) return;
  final status = action.data["status"] as int?;
});

Call States

State Code Description
unknown 0 Unknown state
calling 1 Outgoing call initiated
incoming 2 Incoming call
early 3 Ringing
connecting 4 Connecting
confirmed 5 Active call
disconnected 6 Call ended
hold 7 Call on hold

Event Data (onCallStateChanged)

{
  "isVideo": bool,          // true for video call
  "status": int,            // call state code (0-7)
  "callerNumber": String,   // phone number
  "incoming": bool,         // true if incoming
  "_id": String,            // iOS: call UUID; Android: ""
  "transactionId": String,  // iOS: every event; Android: on CONFIRMED
  // When status == disconnected (6):
  "code_end_call": int,     // SIP/OMI end reason code β€” see Call End Codes table
}

DISCONNECTED arrives twice on iOS: once with the call info + code_end_call, and once from the SDK teardown as {status: 6, message, codeEndCall}. Android sends it once. Handle DISCONNECTED idempotently (e.g. close the call screen only once). iOS forwards the raw SDK state, so ignore values you do not know.

State Lifecycle

Outgoing Call:

CALLING (1) β†’ EARLY (3) β†’ CONNECTING (4) β†’ CONFIRMED (5) β†’ DISCONNECTED (6)

Incoming Call:

INCOMING (2) β†’ CONNECTING (4) β†’ CONFIRMED (5) β†’ DISCONNECTED (6)

πŸ“Œ Other Event Listeners #

OmicallClient.instance.setCallQualityListener((data) {
  final info = _qualityTracker.parseCallQuality(data);

  // Use parsed info
  print(info.mosDisplay);      // "4.5"
  print(info.qualityText);     // "Excellent"
  print(info.shouldShowLoading); // true/false
});

Raw data format (if not using helper):

{
  "quality": int,        // 0: GOOD, 1: NORMAL, 2: BAD
  "stat": {
    "req": double,       // Request time
    "mos": double,       // MOS score (1.0-5.0)
    "jitter": double,    // Jitter (ms)
    "latency": double,   // Latency (ms)
    "ppl": double,       // Packet loss (%)
    "lcn": int          // Loss connect count
  },
  "isNeedLoading": bool  // (Deprecated: Use CallQualityTracker instead)
}

Speaker Listener

OmicallClient.instance.setSpeakerListener((isSpeakerOn) {
  setState(() {
    isSpeaker = isSpeakerOn;
  });
});

Mute Listener

OmicallClient.instance.setMuteListener((isMuted) {
  setState(() {
    this.isMuted = isMuted;
  });
});

Hold Listener

OmicallClient.instance.setHoldListener((isOnHold) {
  setState(() {
    this.isHold = isOnHold;
  });
});

Consult / Transfer / Call Hold Listeners (3.4.0)

OmicallClient.instance.setConsultStateListener((OmiConsultStateEvent e) {});   // CONSULT_STATE_CHANGED
OmicallClient.instance.setTransferStatusListener((OmiTransferStatusEvent e) {}); // TRANSFER_STATUS
OmicallClient.instance.setCallHoldChangedListener((OmiCallHoldEvent e) {});    // CALL_HOLD_CHANGED: any call, with callId

Remove with removeConsultStateListener(), removeTransferStatusListener(), removeCallHoldChangedListener().

Remote Video Ready Listener

// data: {"name": "REMOTE_VIDEO_READY", "data": {...}} β€” once per call, Android and iOS
OmicallClient.instance.setVideoListener((data) {
  setState(() => _remoteVideoReady = true); // e.g. hide the "connecting video" placeholder
});

Missed Call Listener

Triggered when user taps missed call notification:

OmicallClient.instance.setMissedCallListener((data) {
  final String callerNumber = data["callerNumber"];
  final bool isVideo = data["isVideo"];
  makeCallWithParams(context, callerNumber, isVideo);
});

Call Log Listener (iOS Only)

Triggered when the user starts a call from the iOS Recents list. Requires processUserActivity in AppDelegate and NSUserActivityTypes β†’ INStartCallIntent in Info.plist (see iOS setup):

OmicallClient.instance.setCallLogListener((data) {
  final String callerNumber = data["callerNumber"];
  final bool isVideo = data["isVideo"];
  makeCallWithParams(context, callerNumber, isVideo);
});

Error Codes #

Call End Codes (code_end_call) #

The code_end_call field is included in the event data when status == disconnected (6). It indicates why the call ended.

How to handle end-call reasons

import 'package:omicall_flutter_plugin/action/action_model.dart'; // OmiCallState

OmicallClient.instance.callStateChangeEvent.listen((omiAction) {
  if (omiAction.actionName != OmiEventList.onCallStateChanged) return;
  final data = omiAction.data;
  final status = data['status'] as int;

  if (status == OmiCallState.disconnected.rawValue) {
    // iOS also sends a second DISCONNECTED with `codeEndCall` (see Event Data)
    final code = (data['code_end_call'] ?? data['codeEndCall']) as int?;
    final reason = _endCallReason(code);
    if (reason != null) {
      // Show toast or dialog to user
      showToast(reason);
    }
  }
});

String? _endCallReason(int? code) {
  switch (code) {
    case null:
    case 0:
    case 200:  // Normal BYE β€” both sides hung up
    case 487:  // Caller cancelled before answer
      return null;
    case 408: return 'No answer. The call was not answered.';
    case 480: return 'Callee is temporarily unavailable.';
    case 486: return 'Line busy. Please try again later.';
    case 503: return 'Service unavailable.';
    default:  return 'Call ended (code: $code).';
  }
}

Standard OMISIP Codes

Code Meaning Action for user
200 Normal call end (BYE) No toast needed
403 Forbidden β€” service plan restricts this number Upgrade service plan
404 Number not found or not allowed to call carrier Check number
408 Request timeout β€” callee did not answer Notify: no answer
480 Temporarily unavailable β€” callee offline/busy Notify: unavailable
486 Busy here β€” callee rejected / busy Notify: line busy
487 Request terminated β€” caller cancelled No toast needed
503 Service unavailable Notify: service error
601 Call ended by customer
602 Call ended by another employee
603 Call rejected β€” check account limit or call barring
850 Simultaneous call limit exceeded
851 Call duration limit exceeded
852 Service package not assigned
853 Internal number disabled
854 Subscriber in DNC (Do Not Call) list
855 Exceeded allowed calls for trial package
856 Exceeded allowed minutes for trial package
857 Subscriber blocked in configuration
858 Unidentified or unconfigured number
859 No available numbers for Viettel direction
860 No available numbers for VinaPhone direction
861 No available numbers for Mobifone direction
862 Temporary block on Viettel direction
863 Temporary block on VinaPhone direction
864 Temporary block on Mobifone direction
865 Advertising number outside permitted calling hours

Troubleshooting #

Common Issues #

1. Call Not Starting (startCall returns error)

Symptom: startCall() returns status other than 8

Solutions:

// Check result and handle errors
final result = await OmicallClient.instance.startCall(phone, false);
final jsonMap = jsonDecode(result as String) as Map;
final status = jsonMap['status'];

// Use 'message' field for cross-platform handling (status code differs between Android/iOS)
final message = jsonMap['message'];

switch(message) {
  case 'START_CALL_SUCCESS':
    navigateToCallScreen();
    break;
  case 'NO_NETWORK':
    // Android: status=408 / iOS: status=11
    print("No network connection - check internet");
    showNoNetworkDialog();
    break;
  case 'PERMISSION_DENIED':
    print("Microphone permission denied");
    await requestMicrophonePermission();
    break;
  case 'COULD_NOT_FIND_END_POINT': // iOS
    print("Not logged in - call register() first");
    break;
  case 'INVALID_PHONE_NUMBER':
  case 'INVALID_UUID':
  case 'CAN_NOT_CALL_YOURSELF':                 // Android
  case 'SAME_PHONE_NUMBER_WITH_PHONE_REGISTER': // iOS
    print("Invalid destination");
    break;
  case 'MAX_RETRY':             // iOS
  case 'REGISTER_ACCOUNT_FAIL': // iOS
  case 'START_CALL_FAIL':       // iOS
    print("Could not start the call - try again");
    break;
  case 'HAVE_ANOTHER_CALL':
    print("Already in a call");
    break;
  case 'SWITCHBOARD_NOT_CONNECTED':
    print("Switchboard not connected - check account");
    break;
  case 'SWITCHBOARD_REGISTERING':
    // status=8 on Android, call is being registered
    navigateToCallScreen();
    break;
  case 'ACCOUNT_TURN_OFF_NUMBER_INTERNAL':
    print("Internal call feature is disabled for this account");
    break;
}

2. No Incoming Call Notification

Android:

  • βœ… Check google-services.json exists in android/app/
  • βœ… Verify the FCM token is registered: OmicallClient.instance.register(..., fcmToken: token)
  • βœ… Check AndroidManifest.xml has FirebaseMessageReceiver
  • βœ… Ensure app has notification permissions

iOS:

  • βœ… Check APNS certificate is configured on Firebase
  • βœ… Verify APNS token: FirebaseMessaging.instance.getAPNSToken()
  • βœ… Check PushKitManager is initialized in AppDelegate
  • βœ… Verify VoIP push certificate in Apple Developer Portal

3. Call Quality Issues (Low MOS Score)

Symptom: MOS < 3.0 or frequent loading indicators

Debugging:

OmicallClient.instance.setCallQualityListener((data) {
  final info = _qualityTracker.parseCallQuality(data);

  print("MOS: ${info.mos}");           // Target: β‰₯ 4.0
  print("Jitter: ${info.jitter}ms");   // Target: < 30ms
  print("Latency: ${info.latency}ms"); // Target: < 150ms
  print("Packet Loss: ${info.packetLoss}%"); // Target: < 1%
  print("LCN: ${info.lcn}");           // Target: 0 or changing

  if (info.mos < 3.0) {
    // Poor call quality detected
    if (info.jitter > 50) {
      print("High jitter - check network stability");
    }
    if (info.latency > 200) {
      print("High latency - check internet speed");
    }
    if (info.packetLoss > 3) {
      print("Packet loss - check WiFi signal");
    }
  }

  if (info.shouldShowLoading) {
    print("Network stuck - LCN frozen at ${info.lcn}");
  }
});

Solutions:

  • Switch from WiFi to cellular or vice versa
  • Close bandwidth-heavy apps
  • Move closer to WiFi router
  • Check internet speed (minimum 100kbps recommended)

4. Video Call Issues

No Remote Video:

  • βœ… Show both RemoteCameraView and LocalCameraView β€” they attach to the SDK by themselves (since 3.4.0 refresh() is optional).
  • βœ… setVideoListener fires REMOTE_VIDEO_READY once the first remote frame is decoded; if it never comes, check the network / PBX video codec (H.264).

Camera Not Switching:

// Ensure camera is created before switching
if (_localController != null) {
  OmicallClient.instance.switchCamera();
}

Black Screen:

  • βœ… Check camera permissions
  • βœ… Ensure isVideo: true in register() and startCall()
  • βœ… Keep both camera widgets on screen during the video call (they attach automatically)

5. Audio Issues

No Audio During Call:

// Check if muted
OmicallClient.instance.setMuteListener((isMuted) {
  if (isMuted) {
    OmicallClient.instance.toggleAudio(); // Unmute
  }
});

// Check speaker status
OmicallClient.instance.setSpeakerListener((isSpeakerOn) {
  print("Speaker: $isSpeakerOn");
});

Echo or Feedback:

  • Use headphones/earphones
  • Enable speaker phone
  • Check microphone sensitivity

Migration Guide #

Upgrading from 3.3.x to 3.4.0 #

No breaking change. Replace the deprecated init functions:

// Before
final raw = await OmicallClient.instance.initCallWithUserPassword(
  userName: u, password: p, realm: r, host: h, isVideo: true, fcmToken: t);
final ok = (jsonDecode(raw) as Map)['status'] == 200;

// After
final result = await OmicallClient.instance.register(
  userName: u, password: p, realm: r, host: h, isVideo: true, fcmToken: t);
final ok = result.isSuccess;
Deprecated New Parameter renames
initCallWithUserPassword register β€”
initCallWithApiKey registerWithApiKey usrName β†’ fullName, usrUuid β†’ userUuid

Behaviour changes:

  • host is now optional on Android (empty = SDK default) and honored on iOS (was always vh.omicrm.com:5222).
  • transferCall failure no longer ends the call on Android β€” listen to setTransferStatusListener.
  • toggleHold() is ignored while a consult transfer is running (both platforms).

Video calls:

  • registerVideoEvent() / removeVideoEvent() are no longer needed (kept as no-ops) and refresh() is optional β€” the camera widgets attach themselves.
  • REMOTE_VIDEO_READY (setVideoListener) now also fires on Android, once per call.
  • iOS renders through OMIVideoCallManager: do not mirror the local preview yourself.
  • Picture-in-Picture: setVideoPictureInPictureEnabled(true); on Android add the manifest flags and forward onUserLeaveHint / onPictureInPictureModeChanged from MainActivity (see Picture-in-Picture).

Native setup:

  • Android: Maven credentials are omicallUsername / omicallPassword in android/local.properties.
  • iOS: minimum deployment target is now 13.0; run pod install to get OmiKit 1.12.1.
  • iOS: Swift Package Manager is supported. If you enable it and your AppDelegate.m imports omicall_flutter_plugin-Swift.h, switch to the conditional import shown in iOS setup.

Upgrading from 2.x.x to 3.x.x #

Call quality: parse setCallQualityListener data with CallQualityTracker instead of reading the raw map yourself (see Call Quality Monitoring):

final _qualityTracker = CallQualityTracker();

OmicallClient.instance.setCallQualityListener((data) {
  final info = _qualityTracker.parseCallQuality(data);
  if (info.shouldShowLoading) {
    EasyLoading.show();          // Android only, see Loading Logic
  } else if (info.isNetworkRecovered) {
    EasyLoading.dismiss();
  }
  setState(() => callQuality = info.mosDisplay); // "4.5"
});

Support #


License #

Copyright Β© 2021 VIHAT Team. All rights reserved.

3
likes
130
points
139
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

OmiKit Flutter is wrapper SDK for OmiCall for Application easy integrate features of Omicall Like Call App2App/App2Phone, Video Call.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, permission_handler, plugin_platform_interface

More

Packages that depend on omicall_flutter_plugin

Packages that implement omicall_flutter_plugin