hiennguyen92 / flutter_callkit_incoming

Flutter Callkit Incoming
https://pub.dev/packages/flutter_callkit_incoming
MIT License
178 stars 307 forks source link

Audio IOS native #602

Open robsonvasquez opened 2 weeks ago

robsonvasquez commented 2 weeks ago

I’m using WebRTC and flutter_callkit_incoming. When I make a call and answer through iOS's native interface, the audio doesn’t play on the iPhone, but it does for the recipient. It was working perfectly on iOS before 10/20/2024 with the latest versions of both libraries, but then the audio just stopped working on iOS when using CallKit to answer. Switching WebRTC versions sometimes makes the audio work on iOS but not for the recipient, and other versions have the opposite effect. Is anyone else experiencing audio issues with both libraries when answering through iOS's native interface?

Delegate

import UIKit import Flutter

import awesome_notifications //import shared_preferences_ios import shared_preferences_foundation

import CallKit import AVFAudio import PushKit import flutter_callkit_incoming import WebRTC

@UIApplicationMain @objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate, CallkitIncomingAppDelegate {

override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool {

if #available(iOS 10.0, *) {
  UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
}
  GeneratedPluginRegistrant.register(with: self)

SwiftAwesomeNotificationsPlugin.setPluginRegistrantCallback { registry in          
      SwiftAwesomeNotificationsPlugin.register(
        with: registry.registrar(forPlugin: "io.flutter.plugins.awesomenotifications.AwesomeNotificationsPlugin")!)          
      SharedPreferencesPlugin.register(
        with: registry.registrar(forPlugin: "io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin")!)
  }

//Setup VOIP
let mainQueue = DispatchQueue.main
let voipRegistry: PKPushRegistry = PKPushRegistry(queue: mainQueue)
voipRegistry.delegate = self
voipRegistry.desiredPushTypes = [PKPushType.voIP]

//Use if using WebRTC
RTCAudioSession.sharedInstance().useManualAudio = true
RTCAudioSession.sharedInstance().isAudioEnabled = false        

return super.application(application, didFinishLaunchingWithOptions: launchOptions)

}

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

// 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(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) {
    print("didReceiveIncomingPushWith t")
    print(payload.dictionaryPayload)
    print(type)

    guard type == .voIP else {
        completion()
        return
    }

    let uuid = payload.dictionaryPayload["uuid"] as? String ?? ""
    let nameCaller = payload.dictionaryPayload["callerName"] as? String ?? ""
    let handle = payload.dictionaryPayload["handle"] as? String ?? ""
    let isVideo = payload.dictionaryPayload["isVideo"] as? Bool ?? false

    let data = flutter_callkit_incoming.Data(id: uuid, nameCaller: nameCaller, handle: handle, type: isVideo ? 1 : 0)
    //set more data
    data.extra = ["user": "abc@123", "platform": "ios"]
    // //data.iconName = ...
    data.iconName = "60";
    SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(data, fromPushKit: true)

    // DispatchQueue.global().asyncAfter(deadline: .now() + 10.0) {
    //     DispatchQueue.main.async{
    //         completion()
    //     }
    // }

    let id = payload.dictionaryPayload["id"] as? String ?? ""
    let domain = payload.dictionaryPayload["domain"] as? String ?? ""
    let iceTimeout = payload.dictionaryPayload["iceTimeout"] as? String ?? ""
    let ring_time = payload.dictionaryPayload["ring_time"] as? String ?? ""
    let stun = payload.dictionaryPayload["stun"] as? String ?? ""
    let stunURL = payload.dictionaryPayload["stunURL"] as? String ?? ""
    let tcpPort = payload.dictionaryPayload["tcpPort"] as? String ?? ""
    let turn = payload.dictionaryPayload["turn"] as? String ?? ""
    let turnCredential = payload.dictionaryPayload["turnCredential"] as? String ?? ""
    let turnURL = payload.dictionaryPayload["turnURL"] as? String ?? ""
    let turnUsername = payload.dictionaryPayload["turnUsername"] as? String ?? ""
    let wsdomain = payload.dictionaryPayload["wsdomain"] as? String ?? ""
    let sip_username = payload.dictionaryPayload["sip_username"] as? String ?? ""

    let sip_password = payload.dictionaryPayload["sip_password"] as? String ?? ""
    let zona = payload.dictionaryPayload["zona"] as? String ?? ""
    let zone_id = payload.dictionaryPayload["zone_id"] as? String ?? ""
    let conta_id = payload.dictionaryPayload["account"] as? String ?? ""
    let conta = payload.dictionaryPayload["conta"] as? String ?? ""

    // Cria um dicionário com todos os valores
    let arguments: [String: Any] = [
        "domain": domain,
        "iceTimeout": iceTimeout,
        "ring_time": ring_time,
        "stun": stun,
        "stunURL": stunURL,
        "tcpPort": tcpPort,
        "turn": turn,
        "turnCredential": turnCredential,
        "turnURL": turnURL,
        "turnUsername": turnUsername,
        "wsdomain": wsdomain,
        "sip_username": sip_username,
        "sip_password": sip_password,
        "zona": zona,
        "zone_id": zone_id,
        "conta_id": conta_id,
        "conta": conta,
        "id": id,
        "uuid": uuid
    ]

    // Chamar método Flutter aqui
    callFlutterMethod(method: "startSip", arguments: arguments)

    //Make sure call completion()
    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
        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: onAccept", json)
    print("LOG: onAccept")

    // Chamar método Flutter aqui
    callFlutterMethod(method: "onAccept", arguments: json)

    self.performRequest(parameters: json) { result in
        switch result {
        case .success(let data):
            print("Received data onAccept: \(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")
    callFlutterMethod(method: "onDecline", arguments: json)
    self.performRequest(parameters: json) { result in
        switch result {
        case .success(let data):
            print("Received data onDecline: \(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")
    callFlutterMethod(method: "onEnd", arguments: json)
    self.performRequest(parameters: json) { result in
        switch result {
        case .success(let data):
            print("Received data onEnd: \(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")
    callFlutterMethod(method: "onTimeOut", arguments: json)
    self.performRequest(parameters: json) { result in
        switch result {
        case .success(let data):
            print("Received data onTimeOut: \(data)")

        case .failure(let error):
            print("Error: \(error.localizedDescription)")
        }
    }
}

// Func Callback Toggle Audio Session
func didActivateAudioSession(_ audioSession: AVAudioSession) {
    //Use if using WebRTC
    RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession)
    RTCAudioSession.sharedInstance().isAudioEnabled = true
}

func toggleMute(isMuted: Bool) {
let audioSession = AVAudioSession.sharedInstance()

do {
    // // Define a categoria para permitir a gravação
    // try audioSession.setCategory(.playAndRecord, options: [.defaultToSpeaker])

    // // Muda o estado ativo da sessão, sem desativar
    // try audioSession.setActive(true)

    // Alterna o ganho de entrada do microfone
    let inputGain: Float = isMuted ? 0.0 : 1.0
    // if audioSession.isInputGainSettable {
        try audioSession.setInputGain(inputGain)
        print("Microfone \(isMuted ? "mutado" : "desmutado") com sucesso.")
    // } else {
    //     print("Não é possível ajustar o ganho do microfone.")
    // }
} catch let error {
    print("Erro ao alterar o estado do microfone: \(error.localizedDescription)")
}

}

func toggleSpeaker(isSpeaker: Bool) {
let audioSession = AVAudioSession.sharedInstance()

do {
    // Verifica se deve usar o alto-falante
    if !isSpeaker {
        try audioSession.overrideOutputAudioPort(.speaker)
    } else {
        try audioSession.overrideOutputAudioPort(.none) // Volta para o fone de ouvido
    }
    try audioSession.setActive(true)
    print("Áudio roteado para \(isSpeaker ? "alto-falante" : "fone de ouvido").")
} catch let error {
    print("Erro ao alternar para o alto-falante: \(error.localizedDescription)")
}

}

// Func Callback Toggle Audio Session
func didDeactivateAudioSession(_ audioSession: AVAudioSession) {
    //Use if using WebRTC
    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://webhook.site/e32a591f-0d17-469d-a70d-33e9f9d60727") {
        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"])))
    }
}

// Função para chamar o método Flutter
func callFlutterMethod(method: String, arguments: [String: Any]) {
    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController

    let channel = FlutterMethodChannel(name: "flutter_callkit_channel_ios", binaryMessenger: controller.binaryMessenger)

    channel.invokeMethod(method, arguments: arguments) { result in
        print("Result from Flutter: \(String(describing: result))")
    }

    channel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
        // Identifique o método chamado do Flutter
        switch call.method {
            case "onEnd":
                // Aqui você pode processar os argumentos e retornar o resultado
                if let arguments = call.arguments as? [String: Any],
                let callId = arguments["id"] as? String,
                let uuid = UUID(uuidString: callId) {

                    // Exemplo de processamento dos argumentos
                    let endCallAction = CXEndCallAction(call: uuid)
                    // Criar a transação com a ação de encerrar chamada
                    let transaction = CXTransaction(action: endCallAction)
                    // Controlador de chamadas
                    let callController = CXCallController()
                    // Solicitar a transação para encerrar a chamada
                    callController.request(transaction) { error in
                        if let error = error {
                            print("Erro ao encerrar a chamada: \(error.localizedDescription)")
                        } else {
                            print("Chamada encerrada com sucesso.")
                        }
                    }

                    print("Received arguments: \(arguments)")
                    result("Success from iOS") // Retornar um valor para o Flutter
                } else {
                    result(FlutterError(code: "INVALID_ARGUMENT", message: "Invalid arguments", details: nil))
                }

            case "mute":
                if let args = call.arguments as? [String: Any], let isMuted = args["isMuted"] as? Bool {
                    self.toggleMute(isMuted: isMuted)
                    result("Success")
                } else {
                    result(FlutterError(code: "INVALID_ARGUMENT", message: "Invalid mute argument", details: nil))
                }

            case "toggleSpeaker":
                if let args = call.arguments as? [String: Any], let isSpeaker = args["isSpeaker"] as? Bool {
                    self.toggleSpeaker(isSpeaker: isSpeaker)
                    result("Success")
                } else {
                    result(FlutterError(code: "INVALID_ARGUMENT", message: "Invalid speaker argument", details: nil))
                }

            default:
                result(FlutterMethodNotImplemented)
        }
    }

}

}

Amrazyan commented 4 days ago

same here, @robsonvasquez did you find solution for this?

phildupuis commented 1 day ago

I have the same problem 👋

robsonvasquez commented 1 day ago

Hi! I solved my issue by switching to the fixed WebRTC version 7.0.1, as with other versions, the audio would either play only on the recipient’s end or only on the caller’s end, depending on the WebRTC version.

Here’s an example of how the delegate turned out:

import UIKit import Flutter import CallKit import PushKit import WebRTC import AVFAudio import shared_preferences_foundation import awesome_notifications

@UIApplicationMain @objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate, CXProviderDelegate {

let callController = CXCallController()
var provider: CXProvider?

override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {

    if #available(iOS 10.0, *) {
        UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
    }

    GeneratedPluginRegistrant.register(with: self)

    SwiftAwesomeNotificationsPlugin.setPluginRegistrantCallback { registry in          
        SwiftAwesomeNotificationsPlugin.register(
        with: registry.registrar(forPlugin: "io.flutter.plugins.awesomenotifications.AwesomeNotificationsPlugin")!)          
        SharedPreferencesPlugin.register(
        with: registry.registrar(forPlugin: "io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin")!)
    }

    // Configuração do CallKit
    let configuration = CXProviderConfiguration(localizedName: "")
    configuration.iconTemplateImageData = UIImage(named: "icon")?.pngData()
    //configuration.ringtoneSound = "ring.aiff" // Som de toque personalizado, opcional
    configuration.maximumCallGroups = 1
    configuration.maximumCallsPerCallGroup = 1

    // Configuração do PushKit para VoIP
    let mainQueue = DispatchQueue.main
    let voipRegistry: PKPushRegistry = PKPushRegistry(queue: mainQueue)
    voipRegistry.delegate = self
    voipRegistry.desiredPushTypes = [.voIP]

    provider = CXProvider(configuration: configuration)
    provider?.setDelegate(self, queue: mainQueue)

    RTCAudioSession.sharedInstance().useManualAudio = true
    RTCAudioSession.sharedInstance().isAudioEnabled = false

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

// Delegate para o PKPushRegistry - Recebimento de push VoIP
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) {
    print("didReceiveIncomingPushWith t")
    print(payload.dictionaryPayload)
    print(type)
    print("AQUI IOS ")
    print(payload.dictionaryPayload["uuid"])

    guard type == .voIP else { return }

    // let callerName = payload.dictionaryPayload["callerName"] as? String ?? "Unknown Caller"
    // let uuidString = payload.dictionaryPayload["uuid"] as? String ?? UUID().uuidString
    // let uuid = UUID(uuidString: uuidString) ?? UUID()

    let uuidString = payload.dictionaryPayload["uuid"] as? String ?? ""
    let uuid = UUID(uuidString: uuidString)
    let nameCaller = payload.dictionaryPayload["callerName"] as? String ?? ""

    let id = payload.dictionaryPayload["id"] as? String ?? ""
    let domain = payload.dictionaryPayload["domain"] as? String ?? ""
    let iceTimeout = payload.dictionaryPayload["iceTimeout"] as? String ?? ""
    let ring_time = payload.dictionaryPayload["ring_time"] as? String ?? ""
    let stun = payload.dictionaryPayload["stun"] as? String ?? ""
    let stunURL = payload.dictionaryPayload["stunURL"] as? String ?? ""
    let tcpPort = payload.dictionaryPayload["tcpPort"] as? String ?? ""
    let turn = payload.dictionaryPayload["turn"] as? String ?? ""
    let turnCredential = payload.dictionaryPayload["turnCredential"] as? String ?? ""
    let turnURL = payload.dictionaryPayload["turnURL"] as? String ?? ""
    let turnUsername = payload.dictionaryPayload["turnUsername"] as? String ?? ""
    let wsdomain = payload.dictionaryPayload["wsdomain"] as? String ?? ""

    let sip_username = payload.dictionaryPayload["sip_username"] as? String ?? ""

    let sip_password = payload.dictionaryPayload["sip_password"] as? String ?? ""
    let zona = payload.dictionaryPayload["zona"] as? String ?? ""
    let zone_id = payload.dictionaryPayload["zone_id"] as? String ?? ""
    let conta_id = payload.dictionaryPayload["account"] as? String ?? ""
    let conta = payload.dictionaryPayload["conta"] as? String ?? ""

    // Cria um dicionário com todos os valores
    let arguments: [String: Any] = [
        "domain": domain,
        "iceTimeout": iceTimeout,
        "ring_time": ring_time,
        "stun": stun,
        "stunURL": stunURL,
        "tcpPort": tcpPort,
        "turn": turn,
        "turnCredential": turnCredential,
        "turnURL": turnURL,
        "turnUsername": turnUsername,
        "wsdomain": wsdomain,
        "sip_username": sip_username,
        "sip_password": sip_password,
        "zona": zona,
        "zone_id": zone_id,
        "conta_id": conta_id,
        "conta": conta,
        "id": id,
        "uuid": uuidString
    ]

    // let audioSession = AVAudioSession.sharedInstance()
    // do{
    //     try audioSession.setActive(true) 

    //         print("deu")
    // }catch{
    //     print("eroo")
    // }

    // Chamar método Flutter aqui
    callFlutterMethod(method: "startSip", arguments: arguments)

    reportIncomingCall(uuid: uuid!, handle: nameCaller, hasVideo: false)
}

// Função para relatar uma chamada VoIP para o CallKit
func reportIncomingCall(uuid: UUID, handle: String, hasVideo: Bool) {
    let update = CXCallUpdate()
    update.remoteHandle = CXHandle(type: .generic, value: handle)
    update.hasVideo = hasVideo

    provider?.reportNewIncomingCall(with: uuid, update: update) { error in
        if let error = error {
            print("Erro ao reportar chamada: \(error.localizedDescription)")
        } else {
            print("Chamada reportada com sucesso.")
        }
    }
}

func providerDidReset(_ provider: CXProvider) {
    print("Provider did reset")
    // Você pode realizar ações de limpeza aqui, se necessário
}

// Delegate do CallKit - Ações de chamadas
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {

    // Chamar método Flutter aqui
    callFlutterMethod(method: "onAccept", arguments: [:])
    //activateAudioSession()
    // Ação ao atender a chamada
    action.fulfill()
    //startAudioSession()
}

func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
    // Notifica o WebRTC que a sessão de áudio foi desativada
    RTCAudioSession.sharedInstance().audioSessionDidDeactivate(audioSession)

    // Desativa o áudio do WebRTC
    RTCAudioSession.sharedInstance().isAudioEnabled = false
}

func configureAudioSession(){
    let RTCaudioSession = AVAudioSession.sharedInstance()
    do{
        try RTCaudioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.defaultToSpeaker])
        try RTCaudioSession.setActive(true)

        print("aqui deu")
    }catch{
        print("aqui não deu \(error), \(error.localizedDescription)")
    }
}

func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {

    //do {
      configureAudioSession()
        // Notifica o WebRTC que a sessão de áudio foi ativada
        RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession)

        // Habilita o áudio do WebRTC
        RTCAudioSession.sharedInstance().isAudioEnabled = true

    //}catch{
    //    print("erro ao ativar audio")
    //}
}

// Função para ativar/desativar alto-falante
func setSpeakerEnabled(_ enabled: Bool) {
    do {
        if enabled {
            try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
            print("Alto-falante ativado")
        } else {
            try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
            print("Alto-falante desativado")
        }
    } catch {
        print("Erro ao configurar alto-falante: \(error.localizedDescription)")
    }
}

func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
    callFlutterMethod(method: "onEnd", arguments: [:])
    // Ação ao encerrar a chamada
    action.fulfill()
}

// Delegate para atualização de credenciais de push VoIP
func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
    let deviceToken = credentials.token.map { String(format: "%02x", $0) }.joined()
    print("Token do dispositivo VoIP: \(deviceToken)")
    // Enviar token para o servidor, se necessário

    callFlutterMethod(method: "tokenVoip", arguments: ["token":deviceToken])
}

func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
    print("Token de push VoIP invalidado")
}

// Função para chamar o método Flutter
func callFlutterMethod(method: String, arguments: [String: Any]) {
    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController

    let channel = FlutterMethodChannel(name: "flutter_callkit_channel_ios", binaryMessenger: controller.binaryMessenger)

    channel.invokeMethod(method, arguments: arguments) { result in
        print("Result from Flutter: \(String(describing: result))")
    }

    channel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
        // Identifique o método chamado do Flutter
        switch call.method {
            case "onEnd":
                // Aqui você pode processar os argumentos e retornar o resultado
                if let arguments = call.arguments as? [String: Any],
                let callId = arguments["id"] as? String,
                let uuid = UUID(uuidString: callId) {

                    // Exemplo de processamento dos argumentos
                    let endCallAction = CXEndCallAction(call: uuid)
                    // Criar a transação com a ação de encerrar chamada
                    let transaction = CXTransaction(action: endCallAction)
                    // Solicitar a transação para encerrar a chamada
                    self.callController.request(transaction) { error in
                        if let error = error {
                            print("Erro ao encerrar a chamada: \(error.localizedDescription)")
                        } else {
                            print("Chamada encerrada com sucesso.")
                        }
                    }

                    print("Received arguments: \(arguments)")
                    result("Success from iOS") // Retornar um valor para o Flutter

                    self.callFlutterMethod(method: "onEnd", arguments: [:])
                } else {
                    result(FlutterError(code: "INVALID_ARGUMENT", message: "Invalid arguments", details: nil))
                }
        case "onMute":
            // Aqui você pode processar os argumentos e retornar o resultado
            if let arguments = call.arguments as? [String: Any],
            let callId = arguments["id"] as? String,
            let callAction = arguments["action"] as? Bool,
            let uuid = UUID(uuidString: callId) {

                // Exemplo de processamento dos argumentos
                let muteCAllAction = CXSetMutedCallAction(call: uuid, muted: callAction)
                // Criar a transação com a ação de encerrar chamada
                let transaction = CXTransaction(action: muteCAllAction)
                // Solicitar a transação para encerrar a chamada
                self.callController.request(transaction) { error in
                    if let error = error {
                        print("Erro ao mutar a chamada: \(error.localizedDescription)")
                    } else {
                        print("Chamada mutar com sucesso.")
                    }
                }

                print("Received arguments: \(arguments)")
                result("Success from iOS") // Retornar um valor para o Flutter
            } else {
                result(FlutterError(code: "INVALID_ARGUMENT", message: "Invalid arguments", details: nil))
            }
        case "toggleSpeaker":
                    if let args = call.arguments as? [String: Any],
                        let enableSpeaker = args["enable"] as? Bool {
                        self.setSpeakerEnabled(enableSpeaker)
                        result("Speaker toggled to \(enableSpeaker)")
                    } else {
                        result(FlutterError(code: "INVALID_ARGUMENT", message: "Expected boolean argument", details: nil))
                    }
        case "configAudio":
                self.configureAudioSession()
        default:
            result(FlutterMethodNotImplemented)
        }
    }

}

}

Unfortunately, I'm having an audio issue on iOS when making a call from the app to someone. The audio doesn’t work on iOS, and the device’s microphone doesn’t open. This call is made with the app open through Flutter. If anyone manages to resolve this, please let me know.