linphone-iphone icon indicating copy to clipboard operation
linphone-iphone copied to clipboard

Handling SIP Calls and VoIP Notifications in React Native

Open NomanUmar opened this issue 5 months ago • 0 comments

Description:

I am working on a React Native project that integrates Linphone for SIP functionalities and uses CallKit and VoIP push notifications. I am encountering issues when the app is in a killed state. Here’s a detailed description of the problem:

Problem:

When the app is in a killed state, I receive a VoIP push notification, and RNCallKeep successfully reports a new incoming call. However, the SIP login is not performed, and no invite is received. This prevents the SIP call from being established.

Code Snippets:

PushKit Handler:

func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
    print(payload.dictionaryPayload)
    // Ensure payload data contains necessary fields
    guard let payloadData = payload.dictionaryPayload as? [String: Any],
          let aps = payloadData["aps"] as? [String: Any],
          let handle = payloadData["handle"] as? String,
          let callUUIDString = payloadData["callUUID"] as? String,
          let callUUID = UUID(uuidString: callUUIDString) else {
      completion()
      return
    }
    
    let uuid = UUID(uuidString: callUUIDString) ?? UUID()
    RNVoipPushNotificationManager.addCompletionHandler(callUUIDString, completionHandler: completion)
    RNVoipPushNotificationManager.didReceiveIncomingPush(with: payload, forType: type.rawValue)
    RNCallKeep.reportNewIncomingCall(callUUIDString, handle: handle, handleType: "generic", hasVideo: false, localizedCallerName: "\(handle)", supportsHolding: true, supportsDTMF: true, supportsGrouping: true, supportsUngrouping: true, fromPushKit: true, payload: nil)
    completion()
}

SIP Module in Swift:

import React
import AVFAudio
import linphonesw

@objc(SipModule)
class SipModule: RCTEventEmitter {
  private let factory = Factory.Instance
  private var core: Core
  private var listener = CoreDelegateStub()
  private var call: Call? {
    get {
      return core.currentCall ?? core.calls.first
    }
  }
    
  override static func moduleName() -> String! {
    return "SipModule"
  }
  
  override func supportedEvents() -> [String]! {
    return ["incomingReceived", "callReleased","connected","NOTIFICATION_ACTION","callAccepted"]
  }
  
  override static func requiresMainQueueSetup() -> Bool {
    return false
  }
  
  override init() {
#if DEBUG
    LoggingService.Instance.logLevel = LogLevel.Debug
#endif
    
    try! core = factory.createCore(configPath: nil, factoryConfigPath: nil, systemContext: nil)
    
    super.init()
    
    listener = CoreDelegateStub(
      onCallStateChanged: {
        (core: Core, call: Call, state: Call.State?, message: String) in
        switch(state) {
        case .IncomingReceived:
          print("incoming call")
          let payload = [
            "displayName": call.remoteAddress?.displayName,
            "username": call.remoteAddress?.username
          ]
          self.sendEvent(withName: "incomingReceived", body: payload)
        case .Released:
          print("released call")
          self.sendEvent(withName: "callReleased", body: [:])
        case .Connected:
          print("connected call")
          let payload = [
            "displayName": call.remoteAddress?.displayName,
            "username": call.remoteAddress?.username
          ]
          self.sendEvent(withName: "connected", body:payload)
        default:
          return
        }
      },
      onAccountRegistrationStateChanged: {
        (core: Core, account: Account, state: RegistrationState, message: String) in
        _ = message
        // If account has been configured correctly, we will go through Progress and Ok states
        // Otherwise, we will be Failed.
        NSLog("New registration state is \(state) for user id \( String(describing: account.params?.identityAddress?.asString()))\n")
        if (state == .Ok) {
          print("Loggedin")
        } else if (state == .Cleared) {
          print("error")
        }  else if (state == .Failed) {
          print(message)
        }
      }
    )
    
    core.addDelegate(delegate: listener)
  }
  
  @objc
  func login(_ username: String, password: String, domain: String) {
    do {
      let authInfo = try Factory.Instance.createAuthInfo(username: username, userid: nil,
                                                         passwd: password, ha1: nil, realm: nil,
                                                         domain: domain,algorithm: nil)
      let params = try core.createAccountParams()
      let identityAddress = try Factory.Instance.createAddress(addr: "sip:\(username)@\(domain)")
      try params.setIdentityaddress(newValue: identityAddress)
      params.registerEnabled = true
      
      let serverAddress = try Factory.Instance.createAddress(addr: "sip:\(domain)")
      try serverAddress.setTransport(newValue: .Udp)
      try params.setServeraddress(newValue: serverAddress)
      
      let account = try core.createAccount(params: params)
      
      core.addAuthInfo(info: authInfo)
      try core.addAccount(account: account)
      core.defaultAccount = account
      try core.start()
    } catch (let error) {
      print("~Error ===> \(error)")
    }
  }
  
  
  @objc
  func startCall(_ phone: String, domain: String) {
    do {
      let address = core.interpretUrl(url: "\(phone)@\(domain)", applyInternationalPrefix: true)
      
      guard let address = address else {
        return
      }
      try address.setDisplayname(newValue: "Caller")
      let params = try core.createCallParams(call: nil)
      let _ = core.inviteAddressWithParams(addr: address, params: params)
    } catch {
      
    }
  }
  
  @objc
  func answerCall() {
    do {
      try call?.accept()
    } catch {
      
    }
  }
  
  @objc
  func endCall() {
    DispatchQueue.main.async {
      do {
        if let call = self.call {
          try call.terminate()
        }
      } catch {
      }
    }
  }
  
  @objc
  func sendDtmf(_ dtmf: String) {
    do {
      try call?.sendDtmf(dtmf: dtmf.utf8CString[0])
    } catch {
      
    }
  }
  
  @objc
  func changeAudioOutput(_ output: Int) {
    call?.outputAudioDevice = core.audioDevices.first { device in
      device.type.rawValue == output
    }
  }
  
  @objc
  func toggleMicrophone() {
    core.micEnabled = !core.micEnabled
  }
  
  @objc
  func pauseCall() {
    do {
      try call?.pause()
    } catch {
      
    }
  }
  
  @objc
  func resumeCall() {
    do {
      try call?.resume()
    } catch {
      NSLog(error.localizedDescription)
    }
  }
  
  @objc 
  func printStatement(){
    print("print call from react native")
  }
}

Steps to Reproduce:

Ensure the app is killed (not running in the background). Send a VoIP push notification to the app. Observe that the notification is received and CallKit displays the incoming call UI. SIP login does not occur, and no invite is processed. Expected Behavior:

When receiving a VoIP push notification, the app should:

Perform the SIP login if not already logged in. Process any incoming invites and establish the call. Actual Behavior:

The app receives the VoIP push notification and shows the CallKit UI. SIP login does not occur, and no invite is received, preventing call establishment. Additional Information:

iOS Version: 16.3.1 Xcode Version: 15.4

NomanUmar avatar Sep 04 '24 16:09 NomanUmar