I’m developing a VoIP app and using CallKit to handle incoming calls. However, I’ve encountered an issue: every time I answer the fourth incoming call, my app crashes. This happens consistently with the fourth incoming call, regardless of the previous ones. The crash doesn’t seem to be tied to a specific call but rather occurs at the point of answering the fourth call.
I’ve reviewed my CallKit implementation, but I’m unable to pinpoint the exact cause of the crash. I suspect the issue may lie in how I’m handling or passing call information to CallKit, but I’m not sure exactly what’s going wrong.
Can anyone assist me in identifying the source of this problem? Is there a known issue with CallKit or a specific area in my code that I should investigate? Any insights or guidance would be greatly appreciated.
Here’s the code I’m using to answer incoming calls:
#CallManager.swift:
import UIKit
import CallKit
import AVFoundation
private let sharedManager = CallManager.init()
protocol CallManagerDelegate : class {
func callDidAnswer()
func callDidEnd()
func callDidHold(isOnHold : Bool)
func callDidFail()
func didUpdateActiveCallId(_ callId: String)
// Neue Methode für Stummschaltung
func callDidMute(isMuted: Bool)
}
class CallManager: NSObject, CXProviderDelegate, AVAudioPlayerDelegate {
static let shared = CallManager()
var activeCallId: String?
var provider: CXProvider?
var callController: CXCallController?
var activeCalls: [UUID: UUID] = [:] // Map zur Verwaltung von Anrufen
// Closure für eingehende Anrufe
var onIncomingCall: ((UUID) -> Void)?
var onOutgoingCall: ((UUID) -> Void)?
// Aktuelle UUID des aktiven Anrufs (optional)
var currentCallID: UUID?
var isSIPRegistered: Bool = false // Füge eine Eigenschaft hinzu, um den SIP-Registrierungsstatus zu speichern.
var muteStatus: [Int32: Bool] = [:]
weak var delegate : CallManagerDelegate?
override init() {
let providerConfiguration = CXProviderConfiguration(localizedName: "MyApp")
providerConfiguration.supportsVideo = true
providerConfiguration.maximumCallsPerCallGroup = 1
providerConfiguration.supportedHandleTypes = [.generic]
provider = CXProvider(configuration: providerConfiguration)
callController = CXCallController()
super.init()
provider?.setDelegate(self, queue: nil)
}
// Methode, um zu überprüfen, ob SIP registriert ist und registrieren, wenn nötig
func checkAndRegisterSIP() {
// Stelle sicher, dass die SIP-Daten im SipManager konfiguriert sind
if SipManager.shared.isConfigured {
if !isSIPRegistered {
// SIP registrieren
print("SIP ist nicht registriert. Versuche Registrierung...")
// Verwende die SIP-Daten aus SipManager
let username = SipManager.shared.sipUsername
let password = SipManager.shared.sipPassword
let registrar = SipManager.shared.sipIp
let port = SipManager.shared.sipPort
// Jetzt können wir die Registrierung des SIP-Accounts selbst durchführen
registerSIPAccount(username: username, password: password, registrar: registrar, port: port)
}
} else {
print("SIP-Konfiguration ist nicht vollständig!")
}
}
// SIP-Account manuell registrieren
func registerSIPAccount(username: String, password: String, registrar: String, port: String) {
// Hier musst du den Code für die SIP-Registrierung mit deinen Daten implementieren.
// Beispiel (mit einer hypothetischen SIP-Bibliothek):
print("Registrierung des SIP-Accounts mit den folgenden Daten:")
print("Username: (username), Registrar: (registrar), Port: (port)")
// Registrierung durchführen – Hier hängt es davon ab, wie du PJSIP oder eine andere SIP-Bibliothek verwendest.
// Zum Beispiel:
// Beispiel mit PJSIP:
// let status = PJSIP.registerAccount(username: username, password: password, registrar: registrar, port: port)
// if status == .success {
// isSIPRegistered = true
// print("SIP-Account erfolgreich registriert")
// } else {
// print("Fehler bei der SIP-Registrierung")
// }
// Hier nehmen wir an, die Registrierung war erfolgreich:
isSIPRegistered = true
print("SIP-Account erfolgreich registriert")
// Wenn der SIP-Account registriert ist, kannst du den CallManager informieren:
accStatusListener(registered: true)
}
// Listener für den Registrierungsstatus (SIP)
func accStatusListener(registered: Bool) {
if registered {
isSIPRegistered = true
print("SIP ist nun registriert.")
} else {
isSIPRegistered = false
print("SIP ist nicht registriert.")
checkAndRegisterSIP() // Versuche, SIP zu registrieren, falls es nicht registriert ist.
}
}
// Halten eines Anrufs
public func setCallOnHold(id: UUID, onHold: Bool) {
print("Set call (onHold ? "on" : "off") hold with ID: (id)")
let holdAction = CXSetHeldCallAction(call: id, onHold: onHold)
let transaction = CXTransaction(action: holdAction)
// Führe die Transaktion zur CallKit-Anfrage aus
callController?.request(transaction) { error in
if let error = error {
print("Error setting call on hold: (error)")
} else {
print("Call (onHold ? "held" : "unheld") successfully with ID: (id)")
self.delegate?.callDidHold(isOnHold: onHold)
}
}
}
public func reportOutgoingCall(id: UUID, handle: String) {
print("Reporting outgoing call with ID: (id) and handle: (handle)")
// Setze die Call-ID und benachrichtige den Delegate
self.activeCallId = id.uuidString
print("Aktuelle Call-ID im CallManager: (self.activeCallId ?? "Kein Wert")")
// Benachrichtige den Delegate, dass die Call-ID gesetzt wurde
if let activeCallId = self.activeCallId {
self.delegate?.didUpdateActiveCallId(activeCallId)
}
// CallKit verwenden, um den Anruf zu melden
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .phoneNumber, value: handle) // Setze den remoteHandle auf die Telefonnummer
update.hasVideo = false
// CallKit Provider melden den ausgehenden Anruf
provider?.reportOutgoingCall(with: id, startedConnectingAt: nil)
// Call-ID speichern oder andere Logik
self.activeCalls[id] = id
print("Outgoing call reported with ID: (id)")
// Jetzt den CallKit-Startaufruf durchführen
if let callController = self.callController {
// Erstelle den CXHandle mit der Telefonnummer
let remoteHandle = CXHandle(type: .phoneNumber, value: handle)
// Erstelle die StartCallAction mit der Call-ID und dem Remote-Handle
let startCallAction = CXStartCallAction(call: id, handle: remoteHandle)
// Wenn du Video unterstützen möchtest, kannst du hier `isVideo` auf true setzen
startCallAction.isVideo = false
// Erstelle eine Transaktion mit der StartCallAction
let transaction = CXTransaction(action: startCallAction)
// Führe die Anfrage zur CallKit-Transaktion durch
callController.request(transaction) { error in
if let error = error {
print("Fehler beim Starten des Anrufs: (error)")
} else {
print("Anruf erfolgreich gestartet: (id)")
}
}
} else {
print("CallController ist nicht verfügbar")
}
// Callback nach erfolgreichem Report
self.onOutgoingCall?(id)
}
// Methode zum Melden eines eingehenden Anrufs
public func reportIncomingCall(id: UUID, handle: String) {
print("Reporting call with ID: (id) and handle: (handle)")
// Bevor wir den Anruf melden, prüfen wir, ob SIP registriert ist.
checkAndRegisterSIP() // Überprüfe und registriere SIP, wenn nötig.
currentCallID = id // Speichere die aktuelle UUID
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: handle)
provider?.reportNewIncomingCall(with: id, update: update) { error in
if let error = error {
print("Error reporting incoming call: (error)")
} else {
self.activeCalls[id] = id // Speichere die Call-ID in activeCalls
print("Call reported with ID: (id)")
print("Active calls after reporting: (self.activeCalls)")
// Callback aufrufen
self.onIncomingCall?(id)
print("onIncomingCall callback triggered with ID: (id)")
// Setze die aktive Call-ID hier
DispatchQueue.main.async {
self.activeCallId = id.uuidString // Setze die aktive Call-ID im CallManager
}
}
}
}
// Antwort auf einen Anruf
public func answerCall(id: UUID, completion: @escaping (Bool) -> Void) {
print("Active calls before answering: (self.activeCalls)")
guard let _ = activeCalls[id] else {
print("Call with ID (id) not found!")
completion(false)
return
}
let answerCallAction = CXAnswerCallAction(call: id)
let transaction = CXTransaction(action: answerCallAction)
callController?.request(transaction) { error in
if let error = error {
print("Error answering call: (error)")
completion(false)
} else {
print("Call answered with ID: (id)")
completion(true)
}
}
}
// Beenden eines Anrufs
public func endCall(id: UUID) {
print("Active calls before ending: (self.activeCalls)") // Debugging-Ausgabe
// Überprüfe, ob der Anruf in activeCalls vorhanden ist
guard let uuid = activeCalls[id] else {
print("Call with ID (id) not found!") // Wenn der Anruf nicht gefunden wird
return
}
// Der Anruf ist gefunden, er wird nun beendet
let endCallAction = CXEndCallAction(call: uuid)
let transaction = CXTransaction(action: endCallAction)
// Führe die EndCall-Aktion aus
callController?.request(transaction) { error in
if let error = error {
print("Error ending call: (error)")
} else {
print("Call ended with ID: (id)")
// Entferne den Anruf erst nach Abschluss der EndCall-Aktion
// Entferne nicht sofort, sondern vertraue auf den CXProvider Delegate
}
}
}
// Callback zum Setzen der aktiven Call-ID
func onIncomingCall(id: UUID) {
DispatchQueue.main.async {
self.activeCallId = id.uuidString // Setze die aktive Call-ID im CallManager
print("Aktuelle Call-ID im CallManager: (self.activeCallId ?? "keine ID")")
}
}
// Callback zum Setzen der aktiven Call-ID
func onOutgoingCall(id: UUID) {
DispatchQueue.main.async {
self.activeCallId = id.uuidString // Setze die aktive Call-ID im CallManager
print("Aktuelle Call-ID im CallManager: (self.activeCallId ?? "keine ID")")
}
}
func providerDidReset(_ provider: CXProvider) {
print("Provider did reset. Clearing all active calls.")
activeCalls.removeAll()
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
print("Provider is handling call answer for ID: (action.callUUID)")
// Überprüfe, ob der Anruf in activeCalls vorhanden ist
if let _ = activeCalls[action.callUUID] {
print("Anruf mit ID (action.callUUID) gefunden, Anruf wird beantwortet.")
action.fulfill() // Markiere die Aktion als erledigt
CPPWrapper().answerCall() // Hier führst du die Logik zum Beantworten des Anrufs aus
} else {
print("Kein aktiver Anruf gefunden mit ID: (action.callUUID)")
action.fail() // Falls der Anruf nicht gefunden wird, fehlschlagen
}
}
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
print("Provider is handling call end for ID: (action.callUUID)")
// Überprüfe, ob der Anruf in activeCalls vorhanden ist, bevor er beendet wird
if let _ = activeCalls[action.callUUID] {
playEndCallSound()
print("Anruf mit ID (action.callUUID) wird beendet.")
CPPWrapper().hangupCall() // Hier führst du die Logik zum Beenden des Anrufs aus
activeCalls.removeValue(forKey: action.callUUID) // Entferne den Anruf aus der aktiven Liste
action.fulfill() // Markiere die Aktion als erledigt
} else {
print("Kein aktiver Anruf gefunden mit ID: (action.callUUID)")
action.fail() // Falls der Anruf nicht gefunden wird, fehlschlagen
}
}
var audioPlayer: AVAudioPlayer?
func playEndCallSound() {
// Versuche, die MP3-Datei aus den Ressourcen zu laden
guard let soundURL = Bundle.main.url(forResource: "endCall", withExtension: "mp3") else {
print("Fehler: Ton-Datei 'endCall.mp3' nicht gefunden.")
return
}
// Versuche, den AudioPlayer zu erstellen und die Audiodatei abzuspielen
do {
audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
// Setze die Audio-Player-Optionen (z.B. Lautstärke oder Wiederholung, falls gewünscht)
audioPlayer?.volume = 1.0 // Maximale Lautstärke
audioPlayer?.numberOfLoops = 0 // Die Datei soll nur einmal abgespielt werden
// Starte das Abspielen des Sounds
audioPlayer?.play()
print("End-Call Ton wird abgespielt.")
} catch let error as NSError {
// Fehlerbehandlung falls das Abspielen der Datei fehlschlägt
print("Fehler beim Erstellen des AudioPlayers: (error.localizedDescription)")
}
}
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
guard let confSlotUUID = activeCalls[action.callUUID] else {
print("No active call found for UUID (action.callUUID)")
action.fail()
return
}
let confSlotId = abs(confSlotUUID.hashValue % Int(PJSUA_MAX_CONF_PORTS))
guard confSlotId >= 0 && confSlotId < Int(PJSUA_MAX_CONF_PORTS) else {
print("Calculated confSlotId (confSlotId) is out of valid range.")
action.fail()
return
}
let slotId = Int32(confSlotId)
// Überprüfung des gewünschten Status
if action.isMuted {
// Nur stummschalten, wenn es aktuell nicht stumm ist
if muteStatus[slotId] != true {
CPPWrapper().muteMicrophoneWrapper(slotId)
muteStatus[slotId] = true
print("Muted microphone for confSlotId: (slotId)")
} else {
print("Microphone is already muted for confSlotId: (slotId)")
}
} else {
// Überprüfung, ob der Anruf aktiv ist und ob das Entstummen erlaubt ist
if let activeCall = activeCalls[action.callUUID], activeCall == confSlotUUID {
// Nur entstummen, wenn es aktuell stumm ist
if muteStatus[slotId] != false {
CPPWrapper().unmuteMicrophoneWrapper(slotId)
muteStatus[slotId] = false
print("Unmuted microphone for confSlotId: (slotId)")
} else {
print("Microphone is already unmuted for confSlotId: (slotId)")
}
} else {
print("Skipping unmute action as the call is no longer active.")
}
}
// Informiere den Delegate über die Aktion
delegate?.callDidMute(isMuted: action.isMuted)
action.fulfill()
}
}
#IncomingViewController.swift:
import CallKit
class IncomingViewController: UIViewController {
@IBOutlet weak var holdButton: UIButton!
var holdFlag: Bool = false
var incomingCallId: String = ""
@IBOutlet weak var callTitle: UILabel!
var activeCallId: String? // Diese Variable wird jetzt von callManager verwaltet
var callManager = CallManager.shared
var id = UUID()
var isMuted = false
@IBOutlet weak var activeCallTitle: UILabel!
@IBOutlet weak var muteButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
activeCallTitle.text = callManager.activeCallId // Zugriff auf activeCallId von callManager
callTitle.text = incomingCallId
// Beobachter für den Proximity-Sensor
NotificationCenter.default.addObserver(self,
selector: #selector(proximityChanged),
name: UIDevice.proximityStateDidChangeNotification,
object: nil)
// CPPWrapper zum Handling des Anrufstatus
CPPWrapper().call_listener_wrapper(call_status_listener_swift)
}
// Beobachter-Methode für den Proximity-Sensor
@objc func proximityChanged(notification: Notification) {
if UIDevice.current.proximityState {
// Der Proximity Sensor ist aktiv (z.B. das Handy wird ans Ohr gehalten)
print("Proximity sensor activated - Display ausschalten")
// Hier kannst du den Bildschirm deaktivieren, z.B. durch dimmen oder Ausschalten
} else {
// Der Proximity Sensor ist nicht aktiv (z.B. das Handy wird entfernt)
print("Proximity sensor deactivated - Display wieder einschalten")
// Hier kannst du das Display wieder aktivieren
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Proximity-Sensor deaktivieren
UIDevice.current.isProximityMonitoringEnabled = false
// Stummschaltung bleibt erhalten
if isMuted {
// Wenn der Anruf stummgeschaltet ist, Mute-Status nicht ändern
print("Anruf war stummgeschaltet. Beibehalten.")
} else {
print("Anruf war nicht stummgeschaltet.")
}
// Sofortige Ausführung der Aktionen
guard let callId = self.callManager.activeCallId, let uuid = UUID(uuidString: callId) else {
print("Keine gültige Call-ID zum Auflegen gefunden")
return
}
// Den CallManager benachrichtigen, dass der Anruf beendet wird
self.callManager.endCall(id: uuid)
// Beobachter entfernen
NotificationCenter.default.removeObserver(self,
name: UIDevice.proximityStateDidChangeNotification,
object: nil)
}
@IBAction func hangupClick(_ sender: UIButton) {
// Überprüfe, ob eine gültige Call-ID vorhanden ist
guard let callId = callManager.activeCallId, let uuid = UUID(uuidString: callId) else {
print("Keine gültige Call-ID zum Auflegen gefunden")
return
}
// Den CallManager benachrichtigen, dass der Anruf beendet wird
callManager.endCall(id: uuid) // Call beenden mit der UUID des Anrufs
// Dismiss den ViewController, um den Bildschirm zu schließen
self.dismiss(animated: false, completion: nil)
}
@IBAction func answerClick(_ sender: UIButton) {
// Den CallKit Anruf akzeptieren
guard let callId = UUID(uuidString: incomingCallId) else {
print("Ungültige Call ID")
return
}
// CallManager verwenden, um den Anruf zu starten
callManager.answerCall(id: callId, completion: { success in
if success {
// Nur den Proximity-Sensor aktivieren, wenn der Anruf erfolgreich angenommen wurde
UIDevice.current.isProximityMonitoringEnabled = true
print("Anruf erfolgreich angenommen: (callId)")
} else {
print("Fehler beim Annehmen des Anrufs mit ID: (callId)")
}
})
}
@IBAction func holdClick(_ sender: Any) {
// Überprüfe, ob holdButton korrekt verbunden ist
guard let button = holdButton else {
print("Error: holdButton is not connected.")
return
}
// Das Systembild "pause" verwenden (System-Image)
guard let pauseImage = UIImage(systemName: "pause") else {
print("Error: pause system image is missing.")
return
}
// Überprüfe, ob der CallManager eine aktive Call-ID hat
guard let callUUIDString = CallManager.shared.activeCallId,
let callUUID = UUID(uuidString: callUUIDString) else {
print("Error: Keine gültige aktive Call-ID gefunden.")
return
}
// Anruf halten oder wieder aufnehmen
if holdFlag == false {
// Anruf auf "Hold" setzen
CallManager.shared.setCallOnHold(id: callUUID, onHold: true)
CPPWrapper().holdCall()
// Button-Titel ändern
button.setTitle("Fortsetzen", for: .normal)
// Bildfarbe auf blau ändern (Tint Color anpassen)
button.setImage(pauseImage.withRenderingMode(.alwaysTemplate), for: .normal)
button.tintColor = UIColor.systemBlue
} else {
// Anruf wieder aufnehmen
CallManager.shared.setCallOnHold(id: callUUID, onHold: false)
CPPWrapper().unholdCall()
// Button-Titel zurück zu "Halten"
button.setTitle("Halten", for: .normal)
// Bildfarbe zurück auf schwarz ändern (Tint Color auf schwarz setzen)
button.setImage(pauseImage.withRenderingMode(.alwaysTemplate), for: .normal)
button.tintColor = UIColor.label // Setze die Farbe auf Schwarz
}
holdFlag.toggle()
}
@IBAction func muteButton(_ sender: Any) {
let callManager = CallManager.shared
// Überprüfen, ob der Sender ein UIButton ist
guard let button = sender as? UIButton else {
print("Error: Sender is not a UIButton.")
return
}
// Holen Sie die aktive Call UUID
guard let activeCallUUID = callManager.activeCalls.keys.first else {
print("No active call found to toggle mute!")
return
}
// Toggle den Zustand
isMuted.toggle()
// CallKit-Aktion erstellen
let muteAction = CXSetMutedCallAction(call: activeCallUUID, muted: isMuted)
let transaction = CXTransaction(action: muteAction)
// CallKit-Anfrage senden
callManager.callController?.request(transaction) { error in
if let error = error {
print("Failed to toggle mute: (error)")
self.isMuted.toggle() // Rückgängig machen, falls ein Fehler auftritt
} else {
print(self.isMuted ? "Call muted." : "Call unmuted.")
self.updateMuteButtonUI() // UI aktualisieren
}
}
}
func updateMuteButtonUI() {
DispatchQueue.main.async {
if self.isMuted {
self.muteButton.setTitle("Stumm ein", for: .normal)
self.muteButton.setImage(UIImage(systemName: "mic.slash.fill"), for: .normal)
self.muteButton.tintColor = UIColor.systemBlue // Optional: Blau für stummgeschaltet
} else {
self.muteButton.setTitle("Stumm ein", for: .normal)
self.muteButton.setImage(UIImage(systemName: "mic.fill"), for: .normal)
self.muteButton.tintColor = UIColor.label // Optional: Schwarz für entstummt
}
}
}
}
When the app crashes, I see the following highlighted line of code:
#media.cpp
{
PJSUA2_CHECK_EXPR( pjsua_conf_connect(id, sink.id) );
}
Error:
libc++abi: terminating due to uncaught exception of type pj::Error
How can I solve this? Thank you.
BouncedBy is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.