iosswiftvoipcallkit

Voip call got crashed when app is background and triggered endCall method. (After adding timer for unanswered case)


I'm trying to implement unanswered case for voip call.

When inside reportNewIncomingCall's completion I started internal timer to track timeout for 60sec.

public final class CallCenter: NSObject {
    fileprivate var sessionPool = [UUID: String]()
    
    public func showIncomingCall(of session: String, completion: @escaping () -> Void) {
     let callUpdate = CXCallUpdate()
     callUpdate.remoteHandle = CXHandle(type: .generic, value: session)
     callUpdate.localizedCallerName = session
     callUpdate.hasVideo = true
     callUpdate.supportsDTMF = false
    
     let uuid = pairedUUID(of: session)
     
     provider.reportNewIncomingCall(with: uuid, update: callUpdate, completion: { [unowned self] error in
        if let error = error {
            print("reportNewIncomingCall error: \(error.localizedDescription)")
        }
        // We cant auto dismiss incoming call since there is a chance to get another voip push for cancelling the call screen ("reject") from server.
        let timer = Timer(timeInterval: incomingCallTimeoutDuration, repeats: false, block: { [unowned self] timer in
            self.endCall(of: session, at: nil, reason: .unanswered)
            self.ringingTimer?.invalidate()
            self.ringingTimer = nil
        })
        timer.tolerance = 0.5
        RunLoop.main.add(timer, forMode: .common)
        ringingTimer = timer
        completion()
    })
  }
  public func endCall(of session: String, at: Date?, reason: CallEndReason) {
    let uuid = pairedUUID(of: session)
    provider.reportCall(with: uuid, endedAt: at, reason: reason.reason)
  }
}

When peer user (caller) declined, I will get another voip notification and i'm calling this.

callCenter.endCall(of: caller, at: Date(), reason: .declinedElsewhere)

Scenario:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Killing app because it never posted an incoming call to the system after receiving a PushKit VoIP push.'

Appdelegate:

func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
    print("Payload: \(payload.dictionaryPayload)")
    guard let data = payload.dictionaryPayload as? [String: Any],
          let userID = data["user"] as? UInt64,
          let event = data["event"] as? String,
          let caller = data["callerName"] as? String
    else {
        print("Incoming call failed due to missing keys.")
        callCenter.showIncomingCall(of: "Unknown") { [unowned self] in
            self.callCenter.endCall(of: "Unknown", at: nil, reason: .failed)
            completion()
        }
        return
    }
    
    switch event {
    case "reject":
        callCenter.endCall(of: caller, at: Date(), reason: .declinedElsewhere)
        callingUser = nil
        completion()
        return;
        
    case "cancel":
        callCenter.endCall(of: caller, at: Date(), reason: .answeredElsewhere)
        callingUser = nil
        completion()
        return;
    default: break
    }

    let callingUser = CallingUser(session: caller, userID: userID)
    callCenter.showIncomingCall(of: callingUser.session) {
        completion()
    }
    self.callingUser = callingUser
}

Above scenario works well without unanswered case. Means, i can able to trigger endCall method (with any reason) when app is in background. And it works. So i think issue is with the timer. Basically I'm calling endCall method with same UUID and for different reasons. And its works fine if I remove timer logic.

What's best practice or recommended way to implement unanswered case.? Where did I go wrong?


Solution

  • I can above to resolve this issue by initiating a fake call if there is no Active calls in the app.

    private func showFakeCall(of session: String, callerName: String, completion: @escaping () -> Void) {
        callCenter.showIncomingCall(of: session, callerName: callerName) { [unowned self] in
            self.callCenter.endCall(of: session, at: nil, reason: .failed)
            print("Print end call inside Fakecall")
            completion()
        }
    }
    

    Added following check for all of the call events (reject, cancel)

    if !callCenter.hasActiveCall(of: channelName) {
          print("No Active calls found. Initiating Fake call.")
          showFakeCall(of: channelName, callerName: callerName, completion: completion)
          return
    }
    

    Extra tip: You have to reinstall (uninstall first), if you made any changes to CXProvider/CXCallController configurations.