π¦ 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
- Architecture Overview
- Configuration
- Call Flow Lifecycle
- API Reference
- Event Listeners
- Error Codes
- Troubleshooting
- Migration Guide
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:
-
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
- Without
-
Android 14+ requires foregroundServiceType:
- NotificationService:
android:foregroundServiceType="microphone|phoneCall"(NOT camera) - FirebaseMessageReceiver:
android:foregroundServiceType="remoteMessaging"
- NotificationService:
-
Android 13+ requires POST_NOTIFICATIONS:
- Request this permission at runtime for notifications to work
- Add to your permissions request flow
-
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.ktfile 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.jsoninandroid/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_imageinto 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 addedfirebase_messagingin 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.jsoninandroid/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 addedfirebase_messagingin 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.jsoninandroid/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 supportmissedCallTitle: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
Call Quality Listener (Recommended: Use CallQualityTracker)
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.jsonexists inandroid/app/ - β
Verify FCM token is registered:
OmicallClient.instance.initCall(..., fcmToken: token) - β
Check
AndroidManifest.xmlhasFirebaseMessageReceiver - β Ensure app has notification permissions
iOS:
- β Check APNS certificate is configured on Firebase
- β
Verify APNS token:
FirebaseMessaging.instance.getAPNSToken() - β
Check
PushKitManageris initialized inAppDelegate - β 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: trueininitCall()andstartCall() - β
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
- getInstance() Removed
β Old Code:
OmicallClient.getInstance(context).startCall(phone, false);
β New Code:
OmicallClient.instance.startCall(phone, false);
- 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"
});
});
- 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
-
CallQualityTracker Helper
- Automatic MOS parsing
- Built-in LCN tracking
- Network recovery detection
- See Call Quality Monitoring
-
Enhanced Error Handling
- Better error messages in
startCall() - Detailed error codes
- See Error Codes
- Better error messages in
-
Improved Documentation
- ASCII architecture diagrams
- Call flow lifecycle charts
- Comprehensive troubleshooting
Support
- π§ Email: support@vihat.vn
- π± Hotline: 1900 2929 29
- π Website: https://omicall.com
- π API Docs: https://api.omicall.com
License
Copyright Β© 2024 VIHAT Team. All rights reserved.