πŸ“¦ 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)

πŸ“ Status

Currently active maintenance and improve performance


πŸ“š Table of Contents


Quick Start

Install via pubspec.yaml:

dependencies:
  omicall_flutter_plugin: ^latest_version

Minimum setup:

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

// 2. Login
await OmicallClient.instance.initCall(
  userName: "your_username",
  password: "your_password",
  realm: "your_realm",
  host: "your_host",
  isVideo: false,
  fcmToken: fcmToken,
  projectId: "your_firebase_project_id"
);

// 3. Make a call
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() ──►  HOLD (7)
     β”‚                       β”‚
     β”‚                       β”œβ”€ "Call on hold"
     β”‚                       β”œβ”€ πŸ”‡ Audio muted for both
     β”‚                       β”‚
     β”‚  ◄──── toggleHold() ───
     β”‚
     β–Ό
CONFIRMED (5)
     β”‚
     β”œβ”€ "Call resumed"
     β”œβ”€ πŸ”Š Audio restored

Configuration

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

πŸš€ Android:

πŸ“Œ - Config gradle file
  • Add these settings in build.gradle:
jcenter()
maven {
  url "https://maven.pkg.github.com/omicall/OMICall-SDK"
  credentials {
    username = OMI_USER // Please connect with developer OMI for get information
    password = OMI_TOKEN
  }
  authentication {
    basic(BasicAuthentication)
  }
}
//in dependencies
classpath 'com.google.gms:google-services:4.3.13' // You can choose the version of google-services to suit your project
//under buildscript
allprojects {
    repositories {
        google()
        mavenCentral()
        jcenter() // Warning: this repository is going to shut down soon
        maven {
          url "https://maven.pkg.github.com/omicall/OMICall-SDK"
          credentials {
              username = OMI_USER
              password = OMI_TOKEN
          }
          authentication {
              basic(BasicAuthentication)
          }
        }
    }
}

If you use the latest Flutter using the build.gradle.kts file, the configuration is as follows:

allprojects {
    repositories {
        google()
        mavenCentral()
        maven {
            url = uri("https://maven.pkg.github.com/omicall/OMICall-SDK")
            credentials {
                username = project.findProperty("OMI_USER") as? String ?: ""
                password = project.findProperty("OMI_TOKEN") as? String ?: ""
            }
            authentication {
                create<BasicAuthentication>("basic")
            }
        }
    }
}

You can refer android/build.gradle to know more information.

  • Add these settings in app/build.gradle:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'com.google.gms.google-services'

You can refer android/app/build.gradle to know more information.

πŸ“Œ - 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 2.3.78: 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=".MainApplication"
        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="false">

            <!-- 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"
            android:foregroundServiceType="remoteMessaging"
            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 2.3.78) -->
        </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 2.3.78 for details
  2. Android 14+ requires foregroundServiceType:

    • NotificationService: android:foregroundServiceType="microphone|phoneCall" (NOT camera)
    • FirebaseMessageReceiver: android:foregroundServiceType="remoteMessaging"
  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

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.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)
    }

  // 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(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 variables in Appdelegate.h:
#import <UIKit/UIKit.h>
#import <UserNotifications/UserNotifications.h>
#import <OmiKit/OmiKit-umbrella.h>
#import <OmiKit/Constants.h>
#import <UserNotifications/UserNotifications.h>

PushKitManager *pushkitManager;
CallKitProviderDelegate * provider;
PKPushRegistry * voipRegistry;



  • πŸ“Œ Edit AppDelegate.m:
#import <OmiKit/OmiKit.h>
#import <omicall_flutter_plugin/omicall_flutter_plugin-Swift.h>



  • πŸ“Œ Add these lines into didFinishLaunchingWithOptions:
[OmiClient setEnviroment:KEY_OMI_APP_ENVIROMENT_SANDBOX userNameKey:@"extension" maxCall:1 callKitImage: @"callkit_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>
  • πŸ’‘ 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]];
}

✨ Only use under lines when added firebase_messaging plugin in your project

  • βœ… Setup push notification: We only support Firebase for push notification.

  • βœ… Add google-service.json in android/app (For more information, you can refer firebase_core)

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

  • βœ… 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

var pushkitManager: PushKitManager?
var provider: CallKitProviderDelegate?
var voipRegistry: PKPushRegistry?
  • πŸ“Œ Add these lines into didFinishLaunchingWithOptions:
OmiClient.setEnviroment(KEY_OMI_APP_ENVIROMENT_SANDBOX, prefix: "", userNameKey: "extension", maxCall: 1, callKitImage: "callkit_image" typePushVoip:@"default")
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 {
    var value = SwiftOmikitPlugin.processUserActivity(userActivity: userActivity)
    return value
}
  • 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>
  • πŸ’‘ 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
    }
}

✨ Only use under lines when added firebase_messaging plugin in your project

  • βœ… Setup push notification: We only support Firebase for push notification.

  • βœ… Add google-service.json in android/app (For more information, you can refer firebase_core)

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

  • βœ… 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

  • πŸ“Œ We need you request permission about call before make call:
 + android.permission.CALL_PHONE (for android)
 + Permission.audio
 + Permission.microphone
 + Permission.camera  (if you want to make Video calls)

  • πŸ“Œ 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

  • βœ… func initCall: This func is 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();
    }
    await OmicallClient.instance.initCall(
      userName: String,  // Replace with your username
      password:String, // Replace with your password
      realm: String,  // Replace with your realm
      host: String, // Replace with your host
      isVideo: bool, // true if video call is enabled, otherwise false
      fcmToken: String // For iOS, use APNSToken; for Android, FCM token
      projectId: String // Replace with your Firebase project ID
    );
    // result is true then user login successfully.

  • βœ… func initCallWithApiKey: is usually used for your client, who only has a certain function, calling a fixed number. For example, you can only call your hotline number
    String? token = await FirebaseMessaging.instance.getToken();
    if (Platform.isIOS) {
        token = await FirebaseMessaging.instance.getAPNSToken();
    }
     await OmicallClient.instance.initCallWithApiKey(
      usrName:String, // Replace with your username
      usrUuid: String, // Replace with your user UUID
      isVideo: bool, // true if video call is enabled, otherwise false
      apiKey:String,  // Replace with your API key
      fcmToken: String // Note: with IOS, we need APNSToken, and android is FCM_Token,
      projectId: String // Replace with your Firebase project ID
    );
    // result is true then user login successfully.
  • βœ… Get call when user open app from killed status(only iOS):
final result = await OmicallClient.instance.getInitialCall();
  ///if result is not equal False => have a calling.
  • βœ… Config push notification: With iOS, I only support these keys: prefixMissedCallMessage, missedCallTitle, userNameKey. With Android, We don't support missedCallTitle:
      OmicallClient.instance.configPushNotification(
        notificationIcon : "calling_face", //notification icon on Android
        prefix : "Cuα»™c gọi tα»›i tα»«: ",
        incomingBackgroundColor : "#FFFFFFFF",
        incomingAcceptButtonImage : "join_call", //image name
        incomingDeclineButtonImage : "hangup", //image name
        backImage : "ic_back", //image name: icon of back button
        userImage : "calling_face", //image name: icon of user default
        prefixMissedCallMessage: 'Cuα»™c gọi nhα»‘ tα»«' //config prefix message for the missed call
        missedCallTitle: 'Cuα»™c gọi nhα»‘', //config title for the missed call
        userNameKey: 'uuid', //we have 3 values: uuid, full_name, extension
        channelId: 'channelid.callnotification' // need to use call notification,
        audioNotificationDescription: "" //audio description
        videoNotificationDescription: "" //video description
        representName: "" //  Optional value, if nothing is passed down or nil, will display the employee's name or extension number when a call comes in. If you declare a value, this value will be displayed on CallKit when there is an incoming call
      );
      //incomingAcceptButtonImage, incomingDeclineButtonImage, backImage, userImage: Add these into `android/app/src/main/res/drawble`
    

API Reference

Core Functions

πŸ“Œ Authentication

initCall

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

await OmicallClient.instance.initCall(
  userName: String,
  password: String,
  realm: String,
  host: String,
  isVideo: bool,
  fcmToken: String,
  projectId: String
);
initCallWithApiKey

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

await OmicallClient.instance.initCallWithApiKey(
  usrName: String,
  usrUuid: String,
  isVideo: bool,
  apiKey: String,
  fcmToken: String,
  projectId: String
);
logout

Logout current user

OmicallClient.instance.logout();

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.
);

Return values (OmiStartCallStatus):

Status Code Description
invalidUuid 0 UUID khΓ΄ng hợp lệ (khΓ΄ng tΓ¬m thαΊ₯y trong hệ thα»‘ng)
invalidPhoneNumber 1 Sα»‘ Δ‘iện thoαΊ‘i SIP khΓ΄ng hợp lệ
samePhoneNumber 2 KhΓ΄ng thể gọi cΓΉng sα»‘ Δ‘iện thoαΊ‘i
maxRetry 3 HαΊΏt lượt retry, khΓ΄ng thể khởi tαΊ‘o cuα»™c gọi
permissionDenied 4 Quyền audio bα»‹ tα»« chα»‘i
couldNotFindEndpoint 5 Vui lΓ²ng Δ‘Δƒng nhαΊ­p trΖ°α»›c khi gọi
accountRegisterFailed 6 KhΓ΄ng thể Δ‘Δƒng kΓ½ tΓ i khoαΊ£n
startCallFailed 7 KhΓ΄ng thể bαΊ―t Δ‘αΊ§u cuα»™c gọi
startCallSuccess 8 Cuα»™c gọi bαΊ―t Δ‘αΊ§u thΓ nh cΓ΄ng ⬅️ Use this to navigate
haveAnotherCall 9 Đang cΓ³ cuα»™c gọi khΓ‘c

Important: Wait for status 8 before navigating to call screen!

πŸ“Œ Start Call (UUID)

Call using user UUID (API key only)

final result = OmicallClient.instance.startCallWithUUID(
    uuid,         // user id
    _isVideoCall  // call video or audio
);
πŸ“Œ Join Call

Answer incoming call

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

Hang up current call

OmicallClient.instance.endCall().then((callInfo) {
  // callInfo contains the call details
  print(callInfo);
});

/* 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,
  "sip_user": 111,
  "disposition": "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 number characters (1-9, *, #)

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

Forward call to another employee

OmicallClient.instance.transferCall(phoneNumber: "101");
πŸ“Œ 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" }

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();
πŸ“Œ Register Video Event

Listen for remote video readiness

OmicallClient.instance.registerVideoEvent();
πŸ“Œ Remove Video Event

Remove video event listener

OmicallClient.instance.removeVideoEvent();
πŸ“Œ 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

Refresh camera views when needed

// Refresh remote camera
_remoteController?.refresh();

// Refresh local camera
_localController?.refresh();

πŸ“Š 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

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:

Show Loading:
  β”œβ”€ LCN value stays the same for β‰₯3 consecutive events
  └─ 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 Name Description
onCallStateChanged Call state has changed
onSwitchboardAnswer Switchboard SIP is listening

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            // (optional) call identifier
}

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;
  });
});

Remote Video Ready Listener

OmicallClient.instance.setVideoListener((data) {
  refreshRemoteCamera(); // Refresh remote camera view
  refreshLocalCamera();  // Refresh local camera view
});

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 user taps call log entry:

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)

Code Description
600, 503 Network operator codes or user did not answer
408 Request timeout (usually 30 seconds)
403 Service plan only allows dialed numbers - upgrade required
404 Number not allowed to call carrier
480 Number error - contact support
486 Listener refuses and does not answer
601 Call ended by customer
602 Call ended by other 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 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 = json.decode(result);
final status = jsonMap['status'];

switch(status) {
  case 0: // invalidUuid
    print("Invalid UUID - check user credentials");
    break;
  case 1: // invalidPhoneNumber
    print("Invalid phone number format");
    break;
  case 4: // permissionDenied
    print("Microphone permission denied");
    await requestMicrophonePermission();
    break;
  case 5: // couldNotFindEndpoint
    print("Not logged in - call initCall() first");
    break;
  case 8: // SUCCESS
    navigateToCallScreen();
    break;
}

2. No Incoming Call Notification

Android:

  • βœ… Check google-services.json exists in android/app/
  • βœ… Verify FCM token is registered: OmicallClient.instance.initCall(..., 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:

// Register video event listener
OmicallClient.instance.setVideoListener((data) {
  // Refresh camera views when remote video ready
  _remoteController?.refresh();
  _localController?.refresh();
});

Camera Not Switching:

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

Black Screen:

  • βœ… Check camera permissions
  • βœ… Ensure isVideo: true in initCall() and startCall()
  • βœ… Call registerVideoEvent() before call starts
  • βœ… Refresh camera views when state changes

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 2.3.x to 2.5.x

Breaking Changes

  1. getInstance() Removed

❌ Old Code:

OmicallClient.getInstance(context).startCall(phone, false);

βœ… New Code:

OmicallClient.instance.startCall(phone, false);
  1. Call Quality Monitoring

❌ Old Code (Manual Parsing):

OmicallClient.instance.setCallQualityListener((data) {
  final quality = data["quality"] as int;
  final stat = data["stat"] as Map<String, dynamic>;
  final lcn = stat["lcn"] as int? ?? 0;
  final mos = stat["mos"] as double? ?? 0.0;

  // Manual LCN tracking
  if (lcn == lastLcn && lcn != 0) {
    consecutiveCount++;
    if (consecutiveCount >= 3) {
      showLoading();
    }
  }
});

βœ… New Code (Using Helper):

final _qualityTracker = CallQualityTracker();

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

  if (info.shouldShowLoading) {
    EasyLoading.show();
  } else if (info.isNetworkRecovered) {
    EasyLoading.dismiss();
  }

  setState(() {
    callQuality = info.mosDisplay; // "4.5"
  });
});
  1. Package Name Changes

Update imports if you were using internal classes:

❌ Old:

import 'package:omicall_flutter_plugin/some_internal_class.dart';

βœ… New:

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';

New Features in 2.5.x

  1. CallQualityTracker Helper

  2. Enhanced Error Handling

    • Better error messages in startCall()
    • Detailed error codes
    • See Error Codes
  3. Improved Documentation

    • ASCII architecture diagrams
    • Call flow lifecycle charts
    • Comprehensive troubleshooting

Support


License

Copyright Β© 2024 VIHAT Team. All rights reserved.