Video Calling on iOS Android Video calling on iOS Android

Video Calling on iOS & Android: Flutter Developer Guide

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.

Flutter iOS video calling app

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.

Recent Blogs


Posted

in

by

Tags:

To Know Us Better

Browse through our work.

Explore The Technology Used

Learn about the cutting-edge technology and techniques we use to create innovative software solutions.