Search code examples
iosswiftflutterwebrtccallkit

iOS Callkit system screen, no audio if I wait for connection before calling CXAnswerCallAction::fulfill


I'm trying to integrate Callkit into a Flutter app that uses webRTC for calls and I have an issue with taking calls on locked screen. CXAnswerCallAction requires to have the action.fulfill() method called after the connection is established. Here is a pice of code without waiting for establishment of the connection:

public func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
    guard let call = self.callManager?.callWithUUID(uuid: action.callUUID) else{
        action.fail()
        return
    }

    call.data.isAccepted = true
    self.answerCall = call
    self.callManager?.updateCall(call)
    sendEvent(SwiftCallKeepPlugin.ACTION_CALL_ACCEPT, call.data.toJSON())

    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1200)) {
        self.configureAudioSession()
    }
    action.fulfill()
}

This causes the connection time counter to be immediately visible on the screen, but the user still has to wait for connection establishment and can't hear anything.

Here is the code that waits for the establishment of the connection before calling action.fulfill():

public func waitForConnection(uuid: UUID, action: CXAnswerCallAction) {
    if(self.awaitedConnection.uuid != uuid) {
        action.fail()
    } else if(self.awaitedConnection.isConnected) {
        DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1200)) {
            self.configureAudioSession()
        }
        action.fulfill()
    } else {
        DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1000)) {
            self.waitForConnection(uuid: uuid, action: action)
        }
    }
}


public func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
    guard let call = self.callManager?.callWithUUID(uuid: action.callUUID) else{
        action.fail()
        return

    }

    call.data.isAccepted = true
    self.answerCall = call
    self.callManager?.updateCall(call)
    self.awaitedConnection.uuid = action.callUUID
    self.awaitedConnection.isConnected = false
    sendEvent(wiftCallKeepPlugin.ACTION_CALL_ACCEPT, call.data.toJSON())

    waitForConnection(uuid: action.callUUID, action: action)
}

Unfortunately, though it works great on iOS 15.7, on 17.3 it causes lack of audio, no sound and no recording. I also can't enable it later when the call is ongoing. For reference:

func configureAudioSession(){
    let session = AVAudioSession.sharedInstance()
    do{
        try session.setCategory(AVAudioSession.Category.playAndRecord, options: AVAudioSession.CategoryOptions.allowBluetooth)
        try session.setMode(self.getAudioSessionMode(data?.audioSessionMode ?? "voiceChat"))
        try session.setActive(data?.audioSessionActive ?? true)
        try session.setPreferredSampleRate(data?.audioSessionPreferredSampleRate ?? 44100.0)
        try session.setPreferredIOBufferDuration(data?.audioSessionPreferredIOBufferDuration ?? 0.005)
    }catch{
        print(error)
    }
}

I can see in the docs of action.fulfill() that "You should only call this method from the implementation of a CXProviderDelegate method". I this the reason for the issue? But how can I do it if I need to wait for the connection asynchronously and the provider method is synchronous?


Solution

  • The solution came from here https://github.com/flutter-webrtc/flutter-webrtc/issues/1005.

    I passed these callbacks to a forked version of Callkeep:

        private func onRTCAnsweredCall() {
        let session = RTCAudioSession.sharedInstance()
        session.useManualAudio = true
        session.isAudioEnabled = false
    }
    
    private func onRTCAudioActiveCall(didActivate: AVAudioSession) {
        let session = RTCAudioSession.sharedInstance()
        session.audioSessionDidActivate(didActivate)
        session.isAudioEnabled = true
    }
    

    ....

                    SwiftCallKeepPlugin.sharedInstance?.setOnAnsweredCall(onAnsweredCall: onRTCAnsweredCall)
                SwiftCallKeepPlugin.sharedInstance?.setOnAudioActiveCall(onAudioActiveCall: onRTCAudioActiveCall)
    

    then I used them in these to poviders

    public func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
            guard let call = self.callManager?.callWithUUID(uuid: action.callUUID) else{
                action.fail()
                return
            }
            call.data.isAccepted = true
            self.answerCall = call
            self.callManager?.updateCall(call)
            self.awaitedConnection.uuid = action.callUUID
            self.awaitedConnection.isConnected = false
            onAnsweredCall!()
            sendEvent(SwiftCallKeepPlugin.ACTION_CALL_ACCEPT, call.data.toJSON())
            waitForConnection(uuid: action.callUUID, action: action)
    //        DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1200)) {
    //            self.configureAudioSession()
    //        }
    //        action.fulfill()
        }
    
        public func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
            if(self.answerCall?.hasConnected ?? false){
                senddefaultAudioInterruptionNofificationToStartAudioResource()
                return
            }
            if(self.outgoingCall?.hasConnected ?? false){
                senddefaultAudioInterruptionNofificationToStartAudioResource()
                return
            }
            self.outgoingCall?.startCall(withAudioSession: audioSession) {success in
                if success {
                    self.callManager?.addCall(self.outgoingCall!)
                }
            }
            self.answerCall?.ansCall(withAudioSession: audioSession) { _ in }
    //        senddefaultAudioInterruptionNofificationToStartAudioResource()
            onAudioActiveCall!(audioSession)
    //        configureAudioSession()
    //        self.sendEvent(SwiftCallKeepPlugin.ACTION_CALL_TOGGLE_AUDIO_SESSION, ["answerCall": self.answerCall?.data.toJSON(), "outgoingCall": self.outgoingCall?.data.toJSON(), "isActivate": true ])
        }