At Sreyas IT Solutions, our flutter developer team ensures seamless video calling on iOS & Android. We optimize foreground ,background and terminated states, delivering smooth, reliable, and uninterrupted communication for users.
Setup and Integration Overview for Flutter Developer
1. flutter_callkit_incoming package:
This Flutter package is used to handle incoming call notifications on iOS and Android devices, integrating with the native CallKit framework. As a flutter developer, you can use it to display the system call UI, manage call states, and ensure smooth user experiences. It helps you build app features that support incoming video calls seamlessly, even when the app is in the background or terminated.
- Add the package
Include flutter_callkit_incoming in your pubspec.yaml dependencies:
dependencies:
flutter_callkit_incoming: ^<latest_version>
- Update Info.plistΒ
Add the following background modes permissions inside the <dict> tag of your ios/Runner/Info.plist file to enable VoIP, audio, and background processing:
<key>UIBackgroundModes</key><array><string>voip</string><string>remote-notification</string><string>audio</string><string>fetch</string><string>processing</string></array>
- Implement CallKit logic and Add Listen Events
Write your Flutter code to initialize and manage incoming calls with flutter_callkit_incoming.
Future<void> showCallkitIncoming(String uuid, Map<String, dynamic> userInfo) async {
print("π showCallkitIncoming called with UUID: $uuid, userInfo: $userInfo");
final params = CallKitParams(
id: uuid,
nameCaller: userInfo['fromName'] ?? 'Unknown',
appName: 'Callkit',
avatar: 'https://i.pravatar.cc/100',
handle: '0123456789',
type: 1,
duration: 30000,
textAccept: 'Accept',
textDecline: 'Decline',
missedCallNotification: const NotificationParams(
showNotification: true,
isShowCallback: true,
subtitle: 'Missed call',
callbackText: 'Call back',
),
extra: {
'roomName': userInfo['roomName'],
'token': userInfo['token'],
},
headers: {'apiKey': 'Abc@123!', 'platform': 'flutter'},
android: const AndroidParams(
isCustomNotification: true,
isShowLogo: false,
ringtonePath: 'system_ringtone_default',
backgroundColor: '#0955fa',
backgroundUrl: 'assets/test.png',
actionColor: '#4CAF50',
textColor: '#ffffff',
),
ios: const IOSParams(
iconName: 'CallKitLogo',
handleType: 'generic',
supportsVideo: true,
maximumCallGroups: 2,
maximumCallsPerCallGroup: 1,
audioSessionMode: 'default',
audioSessionActive: true,
audioSessionPreferredSampleRate: 44100.0,
audioSessionPreferredIOBufferDuration: 0.005,
supportsDTMF: true,
supportsHolding: true,
supportsGrouping: false,
supportsUngrouping: false,
ringtonePath: 'system_ringtone_default',
),
);
await FlutterCallkitIncoming.showCallkitIncoming(params);
print("β
CallKit notification shown");
var _activeCallId;
var _activeRoomName;
var _activeToken;
FlutterCallkitIncoming.onEvent.listen((event) async {
print("π‘ CallKit Event received: ${event?.event}, body: ${event?.body}");
final body = event?.body ?? {};
final extra = body['extra'] ?? {};
// Save room details for later
if(_activeToken ==null && _activeRoomName ==null && _activeCallId == null){
_activeCallId = body['id']?.toString();
_activeRoomName = extra['roomName']?.toString();
_activeToken = extra['token']?.toString();
}else{
print('NOT NULLLLLL');
}
switch (event!.event) {
case Event.actionCallIncoming:
print("π Incoming call saved roomName=$_activeRoomName , ID: $_activeCallId , Token : $_activeToken");
print("π² Incoming callinggggggg");
break;
case Event.actionCallAccept:
print("β
Call accepted");
final roomName = extra['roomName'] ?? _activeRoomName ?? '';
final token = extra['token'] ?? _activeToken ?? '';
print("π‘ Accept call with roomName=$roomName, token=$token");
print("π² CallKit dismissed, navigating to Flutter video screen");
// 2οΈβ£ Make sure navigation happens on main isolate
WidgetsBinding.instance.addPostFrameCallback((_) async {
try {
print("ENtering to video screen");
TimeSlot selectedTimeslot =
await TimeSlotService().getTimeSlotById(roomName);
Get.toNamed('/video-call', arguments: [
{
'timeSlot': selectedTimeslot,
'room': roomName,
'token': token,
}
]);
print("π¬ Navigated to video call screen");
} catch (e) {
print("β Error navigating to video call: $e");
}
});
break;
case Event.actionCallDecline:
case Event.actionCallEnded:
case Event.actionCallTimeout:
print("β Call declined/ended/timeout");
try {
// β
Safely extract values with defaults
final callId = (body['id'] ?? _activeCallId ?? '').toString();
final roomName = _activeRoomName ?? '';
print("roomnameeeee : $roomName");
// 1οΈβ£ End CallKit session if callId is valid
if (callId.isNotEmpty) {
await FlutterCallkitIncoming.endCall(callId);
print("π² CallKit ended for $callId");
}
// 2οΈβ£ Leave Agora call
final engine = createAgoraRtcEngine();
// 3οΈβ£ Remove Firestore room if youβre the host
await cutAgoraCall(engine, roomName);
_activeCallId = null;
_activeRoomName = null;
_activeToken = null;
print("NUll values : $_activeToken , $_activeRoomName , $_activeCallId");
print("β
Call fully terminated");
} catch (e) {
print("β Error cleaning up call: $e");
}
break;
default:
print("β οΈ Unhandled CallKit event: ${event.event}");
}
});
}
- Add the code below at top-level
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
print("Handling a background message: ${message.data}");
if (message.data['type'] == 'call') {
await showCallNotification(message);
}
}
and Call these in main() function:
Register background message handler for FCM
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
Foreground FCM message handler
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
print('π© Foreground FCM message: ${message.data}');
if (message.data['type'] == 'call') {
final uuid = const Uuid().v4();
await showCallkitIncoming(uuid, message.data);
}
});
MethodChannel for iOS native calls
const MethodChannel channel = MethodChannel('your_channel_name');
channel.setMethodCallHandler((call) async {
print("π² Native method call: ${call.method}");
if (call.method == 'showCallNotification') {
print("π© Raw payload from iOS: ${call.arguments}");
final uuid = const Uuid().v4();
await showCallkitIncoming(uuid, Map<String, dynamic>.from(call.arguments));
}
});
2. Cloud Functions:
We use Firebase Cloud Functions to handle backend logic such as sending push notifications for incoming calls.For iOS devices, we use a Curl-like function for VoIP to ensure call delivery. As a flutter developer, you can rely on these serverless functions to build app workflows that deliver call notifications reliably and securely, ensuring timely alerts to the iOS and Android devices regardless of app state.
// -------------------- Endpoint for Notification --------------------app.post("/", async (req, res) => {
try {
console.log('Received call notification request:', JSON.stringify(req.body));
const { doctorName, roomName, token, timeSlotId, userId } = req.body;
// Get user token
const userToken = await getUserTokenById(userId);
if (!userToken) throw new Error(`No token found for user: ${userId}`);
console.log("token user:", userToken);
console.log("UserId :", userId);
const device = await getUserDevice(userId);
console.log("device:",device);
// Conditional call based on platform
if (device == "ios") {
await startIOSVideoCallNotification(roomName, doctorName, token, userId, timeSlotId);
} else {
await startVideoCallNotification(roomName, doctorName, token, userId, timeSlotId);
}
return res.status(200).json({ success: true });
} catch (error) {
console.error("Error in notificationStartAppointment:", error);
return res.status(500).json({ success: false, error: error.message });
}
});
// -------------------- Android / Standard iOS FCM --------------------
async function startVideoCallNotification(roomName, fromName, agoraToken, userId, timeSlotId) {
try {
const userToken = await getUserTokenById(userId);
if (!userToken) throw new Error(`No FCM token found for user: ${userId}`);
console.log("Sending to token:", userToken);
const payload = {
token: userToken.fcmToken || userToken,
notification: {
title: `Incoming call from ${fromName}`,
body: "Please join the consultation session",
},
data: {
id: "c23e0c9a-8f31-4b60-92b9-1a67951e6571",
type: "call",
roomName,
fromName,
token: agoraToken,
timeSlotId,
click_action: "FLUTTER_NOTIFICATION_CLICK",
},
android: { priority: "high", notification: { click_action: "FLUTTER_NOTIFICATION_CLICK" } },
apns: {
headers: { "apns-push-type": "alert", "apns-priority": "10", "apns-topic": "com.astrouser.astrouser" },
payload: { aps: { alert: { title: `Incoming call from ${fromName}`, body: "Please join the consultation session" }, badge: 0, contentAvailable: 1 } }
}
};
console.log("Sending payload:", JSON.stringify(payload, null, 2));
const response = await admin.messaging().send(payload);
console.log("Successfully sent notification:", response);
return response;
} catch (error) {
console.error("Failed to send video call notification:", error);
throw error;
}
}
// -------------------- iOS VoIP Push --------------------
async function startIOSVideoCallNotification(roomName, fromName, agoraToken, userId, timeSlotId) {
try {
const voipToken = await getUserVoipTokenById(userId);
// // Fetch VoIP token directly from Firestore Users collection
// const userDoc = await db.collection("Users").doc(userId).get();
// if (!userDoc.exists) throw new Error(`User document not found: ${userId}`);
if (!voipToken) throw new Error(`No VoIP token found for user: ${userId}`);
console.log("Sending iOS VoIP push to token:", voipToken);
// Fetch cert and key from Firestore
const certDoc = await db.collection("Settings").doc("ios_certificate").get();
if (!certDoc.exists) throw new Error("iOS certificate document not found in Firestore");
const certContent = certDoc.data().certificate;
const keyContent = certDoc.data().key;
if (!certContent || !keyContent) throw new Error("Certificate or key content missing in Firestore");
// Write to temporary files
const certPath = path.join("/tmp", "VOIPCert.pem");
const keyPath = path.join("/tmp", "VOIPKey.pem");
fs.writeFileSync(certPath, certContent);
fs.writeFileSync(keyPath, keyContent);
console.log("VoIP PEM files written to /tmp");
const apnProvider = new apn.Provider({
cert: certPath,
key: keyPath,
production: false, // true for production
});
console.log("β‘ APNs provider initialized");
const payload = {
id: "c23e0c9a-8f31-4b60-92b9-1a67951e6571", // unique call ID
nameCaller: fromName, // must match AppDelegate parsing
handle: roomName, // roomName for your app
isVideo: true, // or false depending on call type
token: agoraToken,
timeSlotId
};
const note = new apn.Notification({
payload: payload,
pushType: "voip",
topic: "com.astrouser.astrouser.voip",
});
console.log("Note:" , note.payload);
const result = await apnProvider.send(note, voipToken);
console.log("APNs VoIP push result:", result);
apnProvider.shutdown();
return result;
} catch (error) {
console.error("Failed to send iOS VoIP notification:", error);
throw error;
}
}
3. AppDelegate.swift Code:
Custom native code is added in the iOS projectβs AppDelegate.swift file to properly initialize and configure CallKit, handle push notification registration, and manage call lifecycle events. This integration helps build app workflows that bridge Flutter with native iOS functionalities required for video calling.
import UIKit
import CallKit
import AVFAudio
import PushKit
import Flutter
import flutter_callkit_incoming
import FirebaseMessaging // <-- Add this import
import FirebaseCore // <-- Add this import
import os.log
import AgoraRtcKit
@main
@objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate, CallkitIncomingAppDelegate {
// Create custom logger
private let logger = OSLog(subsystem: "com.astrouser.astrouser", category: "VoIP")
var agoraKit: AgoraRtcEngineKit?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
NSLog("π Application did finish launching", logger)
print("π Application did finish launching")
// Configure Firebase (must be called first!)
FirebaseApp.configure() // <-- Initialize Firebase SDK
GeneratedPluginRegistrant.register(with: self)
//Setup VOIP
let mainQueue = DispatchQueue.main
let voipRegistry: PKPushRegistry = PKPushRegistry(queue: mainQueue)
voipRegistry.delegate = self
voipRegistry.desiredPushTypes = [PKPushType.voIP]
print("incomingggggggggggg")
agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: "<#Your App ID#>", delegate: nil)
// Register for remote notifications (APNs)
UIApplication.shared.registerForRemoteNotifications()
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// Handle incoming remote notifications (FCM messages)
override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
print("Message in background ") // This will now print when a notification is received in the background
NSLog("π± [FCM] Received remote notification in background/foreground")
var flutterMethodCompletedSuccessfully = true
do {
let data = try JSONSerialization.data(withJSONObject: userInfo, options: [.prettyPrinted])
if let jsonString = String(data: data, encoding: .utf8) {
// print("π¦ userInfo:\n\(jsonString)")
}
} catch {
print("β Failed to convert userInfo to JSON: \(error)")
print("Raw userInfo: \(userInfo)")
}
Messaging.messaging().appDidReceiveMessage(userInfo) // Handle FCM background message data
NSLog("π₯ [FCM] Processed message through Firebase Messaging")
if let messageID = userInfo["gcm.message_id"] as? String {
print("Message ID: \(messageID)")
NSLog("π [FCM] Message ID: %@", messageID)
}
// Pass notification data to Flutter
let channel = FlutterMethodChannel(name: "your_channel_name", binaryMessenger: self.window.rootViewController as! FlutterViewController as! FlutterBinaryMessenger)
print("Channel initialized: \(channel)")
NSLog("π‘ [FCM] Flutter channel initialized successfully: %@", String(describing: channel))
// 6. Invoke Flutter method
channel.invokeMethod("showCallNotification", arguments: userInfo) { result in
if let result = result {
flutterMethodCompletedSuccessfully = false
NSLog("β
[FCM] Flutter method completed with result: %@", "\(result)")
} else {
NSLog("β οΈ [FCM] Flutter method completed with nil result \(flutterMethodCompletedSuccessfully)")
}
}
print("channel222222222222")
NSLog("π [FCM] Notification handling completed")
// Extract values from FCM userInfo
let id = userInfo["id"] as? String ?? UUID().uuidString
let nameCaller = userInfo["fromName"] as? String ?? "Unknown"
let handle = userInfo["roomName"] as? String ?? "Unknown"
let isVideo = true // If you always want video, or map from a flag in payload
NSLog("π [VoIP] Parsed call data - ID: %@, Caller: %@, Handle(roomName): %@, Video: %@",
id, nameCaller, handle, isVideo ? "YES" : "NO")
// Print the roomName explicitly
print("π· [DEBUG] roomName to send to CallKit: \(handle)")
// Prepare CallKit incoming call data
let data = flutter_callkit_incoming.Data(
id: id,
nameCaller: nameCaller,
handle: handle,
type: isVideo ? 1 : 0 // 0 = audio, 1 = video
)
data.extra = [
"user": "abc@123",
"platform": "ios",
"roomName": handle,
"token": userInfo["token"] as? String ?? ""
]
print("π¦ [DEBUG] extra dictionary: \(data.extra)")
NSLog("β οΈ [VoIP] Call Success \(flutterMethodCompletedSuccessfully) ")
// Show CallKit incoming call
if flutterMethodCompletedSuccessfully {
print("flutterMethodCompletedSuccessfully \(flutterMethodCompletedSuccessfully) ")
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(data, fromPushKit: true)
}else{
print("flutterMethodCompletedFailed \(flutterMethodCompletedSuccessfully) ")
}
print("Completeddddddddddd \(flutterMethodCompletedSuccessfully)")
completionHandler(.newData)
}
// Call back from Recent history
override func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard let handleObj = userActivity.handle else {
return false
}
guard let isVideo = userActivity.isVideo else {
return false
}
let objData = handleObj.getDecryptHandle()
let nameCaller = objData["nameCaller"] as? String ?? ""
let handle = objData["handle"] as? String ?? ""
let data = flutter_callkit_incoming.Data(id: UUID().uuidString, nameCaller: nameCaller, handle: handle, type: isVideo ? 1 : 0)
//set more data...
//data.nameCaller = nameCaller
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.startCall(data, fromPushKit: true)
return super.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
// MARK: - Standard APNs Token (For FCM)
override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Foundation.Data) {
// Set APNs token for Firebase Messaging
Messaging.messaging().apnsToken = deviceToken
// Convert token to string (for debugging)
let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
print("APNs Device Token: \(tokenString)")
}
// Handle updated push credentials
func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
print(credentials.token)
let deviceToken = credentials.token.map { String(format: "%02x", $0) }.joined()
print("VoIP token: \(deviceToken)")
NSLog("VoIP token: %@", deviceToken)
//Save deviceToken to your server
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(deviceToken)
}
func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
print("didInvalidatePushTokenFor")
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP("")
}
// Handle incoming pushes
func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Void) {
NSLog("π± [VoIP] Push registry received incoming push with type: %@", type.rawValue)
guard type == .voIP else {
NSLog("β οΈ [VoIP] Received non-VoIP push notification - ignoring")
return
}
// Log full payload for debugging
NSLog("π¦ [VoIP] Full push payload: %@", payload.dictionaryPayload)
// Extract call data
let id = payload.dictionaryPayload["id"] as? String ?? ""
let nameCaller = payload.dictionaryPayload["nameCaller"] as? String ?? ""
let handle = payload.dictionaryPayload["handle"] as? String ?? ""
let isVideo = payload.dictionaryPayload["isVideo"] as? Bool ?? false
NSLog("π [VoIP] Parsed call data - ID: %@, Caller: %@, Handle: %@, Video: %@",
id, nameCaller, handle, isVideo ? "YES" : "NO")
// Prepare call data
let data = flutter_callkit_incoming.Data(id: id, nameCaller: nameCaller, handle: handle, type: isVideo ? 1 : 0)
data.extra = ["user": "abc@123", "platform": "ios"]
NSLog("π [VoIP] Prepared call data: %@", data.description)
// Show incoming call UI
NSLog("π [VoIP] Attempting to show CallKit incoming call UI")
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(data, fromPushKit: true)
// Pass notification data to Flutter
let channel = FlutterMethodChannel(name: "your_channel_name", binaryMessenger: self.window.rootViewController as! FlutterViewController as! FlutterBinaryMessenger)
print("Channel initialized: \(channel)")
NSLog("π‘ [FCM] Flutter channel initialized successfully: %@", String(describing: channel))
// 6. Invoke Flutter method
channel.invokeMethod("showCallNotification", arguments: payload.dictionaryPayload) { result in
if let result = result {
NSLog("β
[FCM] Flutter method completed with result: %@", "\(result)")
} else {
NSLog("β οΈ [FCM] Flutter method completed with nil result ")
}
}
print("channel Notification")
NSLog("β
[VoIP] Successfully processed incoming VoIP push")
completion()
}
// Func Call api for Accept
func onAccept(_ call: Call, _ action: CXAnswerCallAction) {
let json = ["action": "ACCEPT", "data": call.data.toJSON()] as [String: Any]
print("LOG: on Accept")
self.performRequest(parameters: json) { result in
switch result {
case .success(let data):
print("Received data: \(data)")
//Make sure call action.fulfill() when you are done(connected WebRTC - Start counting seconds)
action.fulfill()
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
}
// Func Call API for Decline
func onDecline(_ call: Call, _ action: CXEndCallAction) {
let json = ["action": "DECLINE", "data": call.data.toJSON()] as [String: Any]
print("LOG: onDecline")
self.performRequest(parameters: json) { result in
switch result {
case .success(let data):
print("Received data: \(data)")
//Make sure call action.fulfill() when you are done
action.fulfill()
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
}
// Func Call API for End
func onEnd(_ call: Call, _ action: CXEndCallAction) {
let json = ["action": "END", "data": call.data.toJSON()] as [String: Any]
print("LOG: onEnd")
self.performRequest(parameters: json) { result in
switch result {
case .success(let data):
print("Received data: \(data)")
//Make sure call action.fulfill() when you are done
action.fulfill()
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
}
// Func Call API for TimeOut
func onTimeOut(_ call: Call) {
let json = ["action": "TIMEOUT", "data": call.data.toJSON()] as [String: Any]
print("LOG: onTimeOut")
self.performRequest(parameters: json) { result in
switch result {
case .success(let data):
print("Received data: \(data)")
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
}
// Func Callback Toggle Audio Session
func didActivateAudioSession(_ audioSession: AVAudioSession) {
//Use if using WebRTC
agoraKit?.enableAudio()
//RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession)
//RTCAudioSession.sharedInstance().isAudioEnabled = true
}
// Func Callback Toggle Audio Session
func didDeactivateAudioSession(_ audioSession: AVAudioSession) {
//Use if using WebRTC
agoraKit?.disableAudio()
//RTCAudioSession.sharedInstance().audioSessionDidDeactivate(audioSession)
//RTCAudioSession.sharedInstance().isAudioEnabled = false
}
func performRequest(parameters: [String: Any], completion: @escaping (Result<Any, Error>) -> Void) { if let url = URL(string: "https://events.hiennv.com/api/logs") {
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
//Add header
do {
let jsonData = try JSONSerialization.data(withJSONObject: parameters, options: [])
request.httpBody = jsonData
} catch {
completion(.failure(error))
return
}
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(NSError(domain: "mobile.app", code: 0, userInfo: [NSLocalizedDescriptionKey: "Empty data"])))
return
}
do {
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
completion(.success(jsonObject))
} catch {
completion(.failure(error))
}
}
task.resume()
} else {
completion(.failure(NSError(domain: "mobile.app", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])))
}
}
}
- Setup Functions
- didFinishLaunchingWithOptions: Initializes Firebase,VoIP push registry, remote notifications
- didRegisterForRemoteNotificationsWithDeviceToken: Registers APNs token with Firebase Messaging
- Push Notification Handling
- didReceiveRemoteNotification: Processes FCM background/foreground messages and forwards to Flutter
- pushRegistry(didUpdate:): Handles VoIP token updates
- pushRegistry(didReceiveIncomingPushWith:): Processes incoming VoIP calls and displays CallKit UI
- CallingKit Event Handlers
- onAccept: Handles call acceptance and notifies server
- onDecline: Handles call rejection and notifies server
- onEnd: Handles call termination and notifies server
- onTimeOut: Handles call timeout and notifies server
- Audio Session Management
- didActivateAudioSession: Activates audio session (WebRTC integration point)
- didDeactivateAudioSession: Deactivates audio session (WebRTC integration point)
- Universal Links
- application(continue:): Handles callbacks from call history/recents
- Network Operations
- performRequest: Generic HTTP POST utility for server communication
4. Updating AndroidManifest.xml for Flutter Video Calling:
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
5. The process for creating certificates for VoIP call is explained in the previous document. Please refer .
( 11-iOS Certificates for VoIP Call: From Keychain to Firebase)
This is how the Incoming Call interface appears.

Conclusion:
At Sreyas IT Solutions , we are dedicated to providing seamless and reliable video calling experiences on iOS and Android. By carefully managing foreground ,background and terminated app states, we build app features that ensure users enjoy uninterrupted communication at all times.







