Siprix VoIP SDK for Flutter
Plugin covers all supported platforms and provides unified API/models. It’s available on PubDev as siprix_voip_sdk.
Integrate Flutter plugin into existing application
Add dependency
Add
siprix_voip_sdkas a dependency in your pubspec.yaml file.dependencies: siprix_voip_sdk: ^1.0.32 provider: ^6.1.1
Request authorization for audio/video (only for iOS/MacOS)
Open project (located in <project>/ios/Runner.xcworkspace or <project>/macos/Runner.xcworkspace) in XCode, select build target, then select
Infotab, add NSMicrophoneUsageDescription key, if required video calls add also NSCameraUsageDescription key.See more: Requesting authorization to capture and save media.
Select checkbox Voice over IP in the signing and capabilities (only for iOS)
In Xcode select build target, then select
Signing & Capabilitiestab, add select checkbox Voice over IP.Modify App Sandbox settings (only for MacOS)
In Xcode select build target, then select
Signing & Capabilitiestab, sectionApp Sandbox, enable access to Network and Hardware.Add imports
import 'package:provider/provider.dart'; import 'package:siprix_voip_sdk/accounts_model.dart'; import 'package:siprix_voip_sdk/calls_model.dart'; import 'package:siprix_voip_sdk/logs_model.dart'; import 'package:siprix_voip_sdk/siprix_voip_sdk.dart';
Prepare models
Siprix provides ready to use models as intermediate layer between library’s API and application’s code. Most important are AccountsModel and CallsModel. See also: Own models implementation
AccountsModel accountsModel = AccountsModel(); CallsModel callsModel = CallsModel(accountsModel); runApp( MultiProvider(providers:[ ChangeNotifierProvider(create: (context) => accountsModel), ChangeNotifierProvider(create: (context) => callsModel), ], child: const MyApp(), ));
Initialize SDK instance
Invoke this code from method initState() of the top level widget’s state.
void _initializeSiprix(LogsModel? logsModel) async { InitData iniData = InitData(); iniData.license = "...license-credentials..."; iniData.logLevelFile = LogLevel.none; iniData.logLevelIde = LogLevel.info; //iniData.singleCallMode = false; //iniData.tlsVerifyServer = true; SiprixVoipSdk().initialize(iniData, logsModel); }
Save and restore state
Application should load the state, saved in previous session, and also set callbacks which will store newly added changes. Model has callback
onSaveChanges(jsonStr)which it raises after each change, like add or remove account, add or modify the recent call item etc. App should store received string argument.Here is example of implementation which uses SharedPreferences for saving. Invoke this method after initialize SDK.
void _readSavedState() async { SharedPreferences.getInstance().then((prefs) { //Read saved accounts AccountsModel accModel = context.read<AccountsModel>(); accModel.loadFromJson(prefs.getString('accounts') ?? ''); accModel.onSaveChanges = _saveAccountChanges; //Read saved CDRs CdrsModel cdrs = context.read<CdrsModel>(); cdrs.loadFromJson(prefs.getString('cdrs') ?? ''); cdrs.onSaveChanges = _saveCdrsChanges; }); } void _saveCdrsChanges(String cdrsJsonStr) { SharedPreferences.getInstance().then((prefs) { prefs.setString('cdrs', cdrsJsonStr); }); } void _saveAccountChanges(String accountsJsonStr) { SharedPreferences.getInstance().then((prefs) { prefs.setString('accounts', accountsJsonStr); }); }
Add own implementation
We suggest to use code of example app as base of the own app.
Also here is completed small fragment of code, which can be used as tutorial. It builds simple UI (shown on the image below) with ability to add account, display its registration status, make call, display it’s status, end call.
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:siprix_voip_sdk/accounts_model.dart'; import 'package:siprix_voip_sdk/calls_model.dart'; import 'package:siprix_voip_sdk/logs_model.dart'; import 'package:siprix_voip_sdk/siprix_voip_sdk.dart'; void main() async { AccountsModel accountsModel = AccountsModel(); CallsModel callsModel = CallsModel(accountsModel); runApp( MultiProvider(providers:[ ChangeNotifierProvider(create: (context) => accountsModel), ChangeNotifierProvider(create: (context) => callsModel), ], child: const MyApp(), )); } class MyApp extends StatefulWidget { const MyApp({super.key}); @override State<MyApp> createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { @override void initState() { super.initState(); _initializeSiprix(); } @override Widget build(BuildContext context) { return MaterialApp( title: 'Siprix VoIP app', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo), visualDensity: VisualDensity.adaptivePlatformDensity, ), home: Scaffold(body:buildBody()) ); } Widget buildBody() { final accounts = context.watch<AccountsModel>(); final calls = context.watch<CallsModel>(); return Column(children: [ ListView.separated(shrinkWrap: true, itemCount: accounts.length, separatorBuilder: (BuildContext context, int index) => const Divider(height: 1), itemBuilder: (BuildContext context, int index) { AccountModel acc = accounts[index]; return ListTile(title: Text(acc.uri, style: Theme.of(context).textTheme.titleSmall), subtitle: Text(acc.regText), tileColor: Colors.blue ); }, ), ElevatedButton(onPressed: _addAccount, child: const Icon(Icons.add_card)), const Divider(height: 1), ListView.separated(shrinkWrap: true, itemCount: calls.length, separatorBuilder: (BuildContext context, int index) => const Divider(height: 1), itemBuilder: (BuildContext context, int index) { CallModel call = calls[index]; return ListTile(title: Text(call.nameAndExt, style: Theme.of(context).textTheme.titleSmall), subtitle: Text(call.state.name), tileColor: Colors.amber, trailing: IconButton( onPressed: (){ call.bye(); }, icon: const Icon(Icons.call_end)) ); }, ), ElevatedButton(onPressed: _addCall, child: const Icon(Icons.add_call)), const Spacer(), ]); } void _initializeSiprix([LogsModel? logsModel]) async { InitData iniData = InitData(); iniData.license = "...license-credentials..."; iniData.logLevelFile = LogLevel.info; SiprixVoipSdk().initialize(iniData, logsModel); } void _addAccount() { AccountModel account = AccountModel(); account.sipServer = "192.168.0.122"; account.sipExtension = "1016"; account.sipPassword = "12345"; account.expireTime = 300; context.read<AccountsModel>().addAccount(account) .catchError(showSnackBar); } void _addCall() { final accounts = context.read<AccountsModel>(); if(accounts.selAccountId==null) return; CallDestination dest = CallDestination("1012", accounts.selAccountId!, false); context.read<CallsModel>().invite(dest) .catchError(showSnackBar); } void showSnackBar(dynamic err) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(err))); } }
Note
Code above uses hardcoded SIP credentials sipServer, sipUser, sipPassword which needs to be changed before testing in the local environment.
Own models implementation
Siprix provides ready to use models as intermediate layer between library’s API and application’s code.
Models extending ChangeNotifier and exposed to the app via ChangeNotifierProvider.
But Siprix library doesn’t have any limitations related to using Provider.
When application prefers different approach it’s quite easy to create own models or just invoke library’s methods without intermediate modell layer, like:
int callId = await SiprixVoipSdk().invite(dest) ?? 0; int accId = await SiprixVoipSdk().addAccount(acc) ?? 0;
The same is true for listening events - add own class as listener which will handle events:
SiprixVoipSdk().accListener = MyAccStateListener( regStateChanged : onRegStateChanged );
See more: Key reasons of using model.
Use source code of existing models as implementation hint or ask support@siprix-voip.com in case of difficulties.
Render video
Library provides two classes for rendering video:
SiprixVideoRenderer- holds textureId, created by native code, and receives notifications when changed resolution of the video, rendered on texture.
SiprixVideoView- widget, used for displaying texture of the renderer.
Use them in the following way (also review example app):
Add renderer(s) as members of the widget, which has the same lifetime as CallModel. Number of renderers is the same as number of videos which is required to display in the same time.
For example: when app can connect multiple calls, but speaks only with one of them - is enough to create 2 renderers (for remote and preview videos).
class _SwitchedCallWidgetState extends State<SwitchedCallWidget> { final SiprixVideoRenderer _localRenderer = SiprixVideoRenderer(); final SiprixVideoRenderer _remoteRenderer = SiprixVideoRenderer(); ...
Init renderers
As first argument use callId of the call, whose video should be rendered. Second argument LogsModel is optional and can be skipped. When it’s specified renderer will print init/dispose details.
@override void initState() { super.initState(); _localRenderer.init(SiprixVoipSdk.kLocalVideoCallId, context.read<LogsModel>()); _remoteRenderer.init(widget.myCall.myCallId, context.read<LogsModel>()); }
Build video views
List<Widget> _buildVideoControls() { List<Widget> children = []; if(widget.myCall.hasVideo) { //Received video children.add( Center(child: SiprixVideoView(_remoteRenderer))); //Camera preview children.add( SizedBox(width: 130, height: 100, child: SiprixVideoView(_localRenderer))); } return children; }
Switch calls
Each time when new call created or user switches between few established calls will be created new SwitchedCallWidget and video renderers as members of its state.
Resolve contact names
Often enough app should display some contact name, which matches phone number of the incoming/outgoing call. Plugin implements this using following way:
On initialization stage app set callback
context.read<CallsModel>().onResolveContactName = _resolveContactName;
App implements this callback like show below, but using own data sources for resolving contact names.
String _resolveContactName(String extension) { if(phoneNumber=="100") { return "MyFriend100"; } else if(phoneNumber=="101") { return "MyFriend101"; } else { return ""; } }
Each time when library makes outgoing call or receives incoming it invokes that callback and when name resolved by app - updates related
CallModel.
iOS: integrate PushKit+CallKit
See detailed explanation PushKit+CallKit Implementation details.
Android: Add Firebase push notifications
From your Flutter project directory, run the following command to install the core plugin:
your-flutter-proj$ flutter pub add firebase_core
Import the Firebase core plugin in lib/main.dart file:
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
Prepare
FirebaseOptionsusing data from google-services.json` downloaded on step [1] :const FirebaseOptions gFCMOptions = FirebaseOptions( apiKey: '...', //Copy from `google-services.json` - `client.api_key.current_key` appId: '...', //Copy from `google-services.json` - `client.client_info.mobilesdk_app_id` messagingSenderId: '...',//Copy from `google-services.json` - `project_info.project_number` projectId: '...', //Copy from `google-services.json` - `project_info.project_id` storageBucket: '...' //Copy from `google-services.json` - `project_info.storage_bucket` );
Initialize FCM on app start:
Future<void> _initializeFCM() async { if(Platform.isAndroid) { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(options: gFCMOptions); FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); } } void main() async { //Wait while Firebase initialized await _initializeFCM(); ... }
Note
It’s important to wait on FCM initialization here because on the next stage app will add accounts and needs to get device’s token by invoke
FirebaseMessaging.instance.getToken().Send push token to server (add token to account)
SIP server (PBX) can send notifications to the application running on specific device only when it has push token provided by this app. Typically app send token as header or Contact’s attribute of SIP REGISTER request. Recommended way to implement this is create own Accounts model which extends existing AccountsModel provided by Siprix. Here is example of implementation (copied from published example app)
class AppAccountsModel extends AccountsModel { AppAccountsModel([this._logs]) : super(_logs); final ILogsModel? _logs; @override Future<void> addAccount(AccountModel acc, {bool saveChanges=true}) async { String? token; if(Platform.isIOS) { token = await SiprixVoipSdk().getPushKitToken();//iOS - get PushKit VoIP token }else if(Platform.isAndroid) { token = await FirebaseMessaging.instance.getToken();//Android - get Firebase token } //When resolved - put token into SIP REGISTER request if(token != null) { _logs?.print('AddAccount with push token: $token'); acc.xheaders = {"X-Token" : token};//Put token into separate header //acc.xContactUriParams = {"X-Token" : token};//OR put token into ContactUriParams } return super.addAccount(acc, saveChanges:saveChanges); }
Handle background messages
Code below is running in the background isolate. When received message application’s Activity may not exist and library’s service could be stopped. In this method App should init FCM, init library and load saved accounts.
@pragma('vm:entry-point') Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(options: gFCMOptions); try{ //Siprix initialization same for main and background isolates _MyAppState._initializeSiprix(); //Read accounts to temporary model and register them SharedPreferences prefs = await SharedPreferences.getInstance(); String accJsonStr = prefs.getString('accounts') ?? ''; if(accJsonStr.isNotEmpty) { AppAccountsModel tmpAccsModel = AppAccountsModel(); tmpAccsModel.loadFromJson(accJsonStr); tmpAccsModel.refreshRegistration(); } } on Exception catch (err) { debugPrint('Error: ${err.toString()}'); } }
Android: Customize incoming call notifications
Library provides two ways for customize notifications, which Android app displays when it’s running in background and receives incoming call:
Modify text labels and icon by adding own string/icon resources in the native Android project
Open project in the Android Studio, add string resources as shown on the image below (you can just copy file
res/values/strings.xmlfrom example application). Modify text of the strings as it’s required for your app.Add own
ic_notif_iconicon resource as shown on the image below. See also Create a notification icon.
Add own notification implementation in the source code of native Android project
Open project in the Android Studio, add new source code file with service implementation as shown on the image below (you can just copy file
MyNotifService.ktfrom example application).Modify implementation of the method
displayIncomingCallNotification. Here you can modify formatting of the phone number, disable/enable required options.Add service declaration to the application’s manifest as shown on the image below.
In the Flutter part of the project set newly added service class name
InitData iniData = InitData(); iniData.license = "...license-credentials..."; ... //set own package and class name here iniData.serviceClassName = "com.siprix.voip_sdk_example.MyNotifService"; await SiprixVoipSdk().initialize(iniData, logsModel);
Android: Use Bluetooth audio devices
Library supports using Bluetooth audio devices and ability to switch devices during a call. To enable it:
Add these permissions to the application’s manifest:
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
When call started select Bluetooth device from list of the devices using methods:
context.watch<DevicesModel>().playout //returns list of available devices context.read<DevicesModel>().setPlayoutDevice(index) //set selected audio device
Android: Restore call(s) state on close and re-open activity
When user made or received call and swipes activity out application continues working (service is alive and core sends/receives audio packets of the call). Next tap on the notification (foreground service or ongoing call) causes to re-create Activity, and being opened it doesn’t “know” anything about ongoing calls.
To sync calls state, use the implementation as shown below.
It works in the following way:
App detects ‘Inactive’ state and sends serialized list of calls to the service
Service keeps that data in memory and removes terminated calls
On next Activity start plugin raises onCallsSyncState event, which restores saved calls
class _MyAppState extends State<MyApp> { late final AppLifecycleListener? _listener; @override void initState() { super.initState(); _initializeSiprix(); ... if(Platform.isAndroid) _listener = AppLifecycleListener(onInactive: _onAndroidAppInactive); } @override void dispose() { _listener?.dispose(); super.dispose(); } void _onAndroidAppInactive() async { // Listen to the app lifecycle 'Inactive' state and send calls state to service (Android only) await SiprixVoipSdk().syncCallsState(context.read<AppCallsModel>()); } ... }
Android: Don’t ask camera permission
In case when application doesn’t need to make video calls it can simply remove camera permission from manifest and library will not request it. To do that add the following line to the application’s manifest file:
<uses-permission android:name="android.permission.CAMERA" tools:node="remove"/>
Android: Skip permissions request
In case when app has its own implementation of the permissions request it can disable internal one by adding to application’s manifest:
<meta-data android:name="com.siprix.SkipPermissionRequest" android:value="true"/>
Android: Don’t display Activity on lock screen
In case when it’s not required to activity on lock screen add to application’s manifest:
<meta-data android:name="com.siprix.DontShowWhenLocked" android:value="true"/>
Android: Stop (mute) ringtone of incoming call
App can enable option iniData.listenVolChange (disabled by default).
In this mode library is listening volume change events and when detetected ringtone volume change
(happens when user presses VolumeDown button) it stops playing ringtone.
Also app can add own code of detection VolumeDown button press and invoke SiprixVoipSdk().stopRingtone(); there.
InitData iniData = InitData(); ... iniData.listenVolChange = true; ... await SiprixVoipSdk().initialize(iniData, logsModel);





