iOS

I. Setting up XCode project

Step 1: Download portsipSDK here

Step 2: Declare permission to use Microphone, Camera (for video calling) in the fileInfo.plist

Step 3: Add these libraries and frameworks to TARGETS → App Name in XCode

  • The VideoToolbox, MetalKit, GLKit, libresolv.tbd, libc++.tbd libraries are supported by XCode, just click the "+" sign and search for the library name and add it.

  • PortSIPVoIPSDK.frameworklocated in the folder iOSSampleextracted from the file .zipdownloaded in step 1.

Note TARGETS not PROJECT

Step 4: InBuild SettingsLinkingOther Linker Flags: add value-ObjC

Reference : https://www.portsip.com/docs/sdk/manual/portsip-voip-sdk-user-manual-ios.pdf

The examples from section II onwards are all applied in just one file: AppDelegate.swift

II. Initialize SDK

Step 1 : Declare global variablelet portsipSDK: PortSIPSDK! = PortSIPSDK(). Used to call PortsipSDK APIs.

Step 2: Open the fileRunner-Bridging-Header.h, import this line#import <PortSIPVoIPSDK/PortSIPVoIPSDK.h>

Step 3: Must implement interface:**PortSIPEventDelegate.** in ClassAppDelegate

Copy

func initPortsipSDK() {

  portsipSDK.unInitialize()

  portsipSDK.delegate = self
  portsipSDK.enableCallKit(true)

  portsipSDK.initialize(
    TRANSPORT_TCP, localIP: "0.0.0.0", localSIPPort: Int32(10002), loglevel: PORTSIP_LOG_NONE, logPath: "",
    maxLine: 8, agent: "PortSIP SDK for IOS", audioDeviceLayer: 0, videoDeviceLayer: 0,
    tlsCertificatesRootPath: "", tlsCipherList: "", verifyTLSCertificate: false)

  // Audio related configurations
  portsipSDK.addAudioCodec(AUDIOCODEC_OPUS)
  portsipSDK.addAudioCodec(AUDIOCODEC_G729)
  portsipSDK.addAudioCodec(AUDIOCODEC_PCMA)
  portsipSDK.addAudioCodec(AUDIOCODEC_PCMU)
  portsipSDK.setAudioSamples(20, maxPtime: 60)

  // Video related configurations
  portsipSDK.addVideoCodec(VIDEO_CODEC_H264)
  portsipSDK.addVideoCodec(VIDEO_CODEC_H263_1998)
  portsipSDK.addVideoCodec(VIDEO_CODEC_VP8)
  portsipSDK.addVideoCodec(VIDEO_CODEC_VP9)
  portsipSDK.setVideoBitrate(-1, bitrateKbps: 512)
  portsipSDK.setVideoFrameRate(-1, frameRate: 20)
  portsipSDK.setVideoResolution(480, height: 640)
  portsipSDK.setVideoNackStatus(true)

  portsipSDK.setInstanceId(UIDevice.current.identifierForVendor?.uuidString)

  portsipSDK.setLicenseKey("PORTSIP_UC_LICENSE")
  
}

This function should be called together with extension login.

III. Extension login

// Explain:
// username: extension_number (example: 1234, 5678, ...)
// password: extension_password (example: abcDefg123, xzyqerTT11, ...)
// domain: tenant_domain (example: voip.example.com, ...)

func registerPortsip(username: String, password: String, domain: String) {
  
  portsipSDK.removeUser()
  var code = portsipSDK.setUser(
    username, displayName: username, authName: username, password: password, userDomain: domain,
    sipServer: "sip.etelecom.vn", sipServerPort: Int32(5063),
    stunServer: "", stunServerPort: 0, outboundServer: "", outboundServerPort: 0)
  
  if code != 0 {
    return
  }
  
  portsipSDK.unRegisterServer()
  code = portsipSDK.registerServer(90, retryTimes: 0)
  
}

There should be a variable portsipRegisteredto save the status of whether the portip has been registered successfully or not. If the registration is successful, just call the API refreshRegistration(), no need to go through the 2 steps initPortsipSDK()above registerPortsip().

portsipSDK.refreshRegistration(0)

If you want to re-register from the beginning, you must initialize the SDK again as in section II .

⇒ When login is successful: event onRegisterSuccess()occurs.

⇒ Conversely, when it fails, the event onRegisterFailure()occurs.

IV. Log out of extension

portsipSDK.removeUser()
portsipSDK.unRegisterServer()
portsipSDK.unInitialize()

V. Making an outgoing call

To make a call:

_sessionID = portsipSDK.call(callee: String, sendSdp: Bool, videoCall: Bool)
// _sessionID is a global variable, when any call is created, there will be a session, this _sessionID variable will save the id of that session.

// callee is the phone number the user wants to call.
/ // sendSdp: whether to transmit the Session Description Protocol or not. Usually set to true
// videoCall: whether to make a video call or not.

When the call recipient accepts the call:

event onInviteAnswered()occurs

func onInviteAnswered(
  _ sessionId: Int,
  callerDisplayName: UnsafeMutablePointer<Int8>!,
  caller: UnsafeMutablePointer<Int8>!,
  calleeDisplayName: UnsafeMutablePointer<Int8>!,
  callee: UnsafeMutablePointer<Int8>!,
  audioCodecs: UnsafeMutablePointer<Int8>!,
  videoCodecs: UnsafeMutablePointer<Int8>!,
  existsAudio: Bool,
  existsVideo: Bool,
  sipMessage: UnsafeMutablePointer<Int8>!
) {}

When the call recipient rejects the call/the call is not answered/or for some reason the call is unsuccessful:

event onInviteFailure()occurs

func onInviteFailure(
  _ sessionId: Int,
  reason: UnsafeMutablePointer<Int8>!,
  code: Int32,
  sipMessage: UnsafeMutablePointer<Int8>!
) {}-

Actively end the call:

portsipSDK.hangUp(sessionId: Int)

VI. Receive call (incoming call)

Portsip Mobile Push needs to be applied so that the application can receive calls in inactive cases:

  • User not on application screen

  • Lock screen

  • Kill app

6.1. Set up mobile push

To use mobile push, you need to declare your iOS application to the portsip system through the following steps:

  • Generate certificate for VoIP Push and APN Push for iOS app

  • Create 1 Apple Certificate fileand Apple Private key file (no password)(2 files .pem)

  • Send these 2 files to eTelecom to create more Mobile Push

See details at: https://www.portsip.com/ios-mobile-push-portsip-pbx sections 4,5 and 9

6.2. Reality

When there is a call to the extension:

Event onInviteIncoming()occurs.

func onInviteIncoming(
  _ sessionId: Int,
  callerDisplayName: UnsafeMutablePointer<Int8>!,
  caller: UnsafeMutablePointer<Int8>!,
  calleeDisplayName: UnsafeMutablePointer<Int8>!,
  callee: UnsafeMutablePointer<Int8>!,
  audioCodecs: UnsafeMutablePointer<Int8>!,
  videoCodecs: UnsafeMutablePointer<Int8>!,
  existsAudio: Bool,
  existsVideo: Bool,
  sipMessage: UnsafeMutablePointer<Int8>!
) {
}

To receive calls : need to use Portsip Mobile Push, iOS services like CallKit, ForceBackground, PushKit.

  • Contact eTelecom to create more Mobile Push

  • Enable these features on XCode:

Reference about CallKit: https://developer.apple.com/documentation/callkit(opens new window)

Declare the following global variables:

  • voipRegistry: PKPushRegistry! = PKPushRegistry(queue: DispatchQueue.main). Used to declare receiving notifications from Portsip Mobile Push.

  • _VoIPPushToken: String = "". Token used to receive VoIP notifications.

  • _APNsPushToken: String = "". Token used to receive general notifications.

  • _cxProvider: CXProvider!. Used to display incoming call popup

  • _cxCallController: CXCallController = CXCallController(). Used to manipulate CallKit calls such as hanging up, answering, ...

The class AppDelegateimplements the following interfaces: UIApplicationDelegate, PKPushRegistryDelegate,CXProviderDelegate

class AppDelegateimports the following services: PushKit, UserNotifications,CallKit

There must be at least 2 functions application()in the file.AppDelegate.swift

  • 1 default application function. Here, enable CallKit (to be able to receive calls, make and receive calls in background mode), PushNotification (to use Portsip Mobile Push), ForceBackground (to let the app run in background mode)

func application(
  _ application: UIApplication,
  didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
  UserDefaults.standard.register(defaults: ["CallKit": true])
  UserDefaults.standard.register(defaults: ["PushNotification": true])
  UserDefaults.standard.register(defaults: ["ForceBackground": true])
}
  • 1 application function used to get device token used for receiving notifications

func application(
  _ application: UIApplication,
  didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {

  let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
  _APNsPushToken = tokenParts.joined()

  updatePushStatusToSipServer() **// Header declaration used to call API Register with Portsip. Will be discussed in step 9.**

}

There must be at least 2 functions pushRegistry()in the file.AppDelegate.swift

  • 1 pushRegistry function used to get token used for receiving VoIP notifications

func pushRegistry(_: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for _: PKPushType) {

  let tokenParts = pushCredentials.token.map { data in String(format: "%02.2hhx", data) }
  _VoIPPushToken = tokenParts.joined()
  
  updatePushStatusToSipServer() **// Header declaration used to call API Register with Portsip. Will be discussed in step 9.**

}
  • 1 pushRegistry function is used to receive VoIP notifications from Portsip Mobile Push. When a call comes in, portsip will send a notification to the user's device.

func pushRegistry(_: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for _: PKPushType) {

 **// If you don't want to receive VoIP notifications while the app is active, check this condition.**
  if UIApplication.shared.applicationState == .active {
    return
  }
  processPushMessageFromPortPBX(payload.dictionaryPayload, completion: {}) **// Use CallKit to announce incoming calls. Will be discussed in step 10.**

}

Declare Header with _VoIPPushToken and _APNsPushToken before calling API Registeror RefreshRegistrationportsipSDK (section II step 1).

func updatePushStatusToSipServer() {

  if _VoIPPushToken.isEmpty || _APNsPushToken.isEmpty {
    return
  }
  
  **portsipSDK.clearAddedSipMessageHeaders()**

  let bundleIdentifier: String = Bundle.main.bundleIdentifier!
  let token = "\(_VoIPPushToken)|\(_APNsPushToken)"
  let pushMessage = "device-os=ios;device-uid=\(token);allow-call-push=true;allow-message-push=true;app-id=\(bundleIdentifier)"

  **portsipSDK.addSipMessageHeader(-1, methodName: "REGISTER", msgType: 1, headerName: "x-p-push", headerValue: pushMessage)**

}

Using CallKit to announce incoming calls, it can be implemented as follows:

func processPushMessageFromPortPBX(_ dictionaryPayload: [AnyHashable: Any], completion: () -> Void) {
  /* dictionaryPayload JSON Format
   Payload: {
   "message_id" = "96854b5d-9d0b-4644-af6d-8d97798d9c5b";
   "msg_content" = "Received a call.";
   "msg_title" = "Received a new call";
   "msg_type" = "call";// im message is "im"
   "x-push-id" = "pvqxCpo-j485AYo9J1cP5A..";
   "send_from" = "102";
   "send_to" = "sip:105@portsip.com";
   }
   */
  
  let parsedObject = dictionaryPayload
  let pushId = dictionaryPayload["x-push-id"]
  
  // _callUUID is a global variable used for announcing incoming calls using CallKit
  if pushId != nil {
    let uuidStr = pushId as! String
    _callUUID = UUID(uuidString: uuidStr)
  }
  if _callUUID == nil {
    return
  }
  
  let handle = CXHandle(type: .generic, value: _callerParsed)
  let update = CXCallUpdate()
  update.remoteHandle = handle
  update.supportsDTMF = false
  update.hasVideo = false
  update.supportsGrouping = false
  update.supportsUngrouping = false
  update.supportsHolding = false
    
  let infoDic = Bundle.main.infoDictionary!
  let localizedName = infoDic["CFBundleName"] as! String
    
  let providerConfiguration = CXProviderConfiguration(localizedName: localizedName)
  providerConfiguration.supportsVideo = true
  providerConfiguration.maximumCallsPerCallGroup = 1
  providerConfiguration.supportedHandleTypes = [.phoneNumber]
    
  _cxProvider = CXProvider(configuration: providerConfiguration)
  _cxProvider.setDelegate(self, queue: DispatchQueue.main)
    
  _cxProvider.reportNewIncomingCall(with: _callUUID, update: update, completion: {})

}

Some actions that users can perform when receiving a call from CallKit: Accept (pick up the phone), Reject, Hang up (after accepting), ...

// 1. When the user clicks the Accept Call button ✅
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
  
  if portsipSDK.answerCall(sessionId: Int, videoCall: Bool) {
    action.fulfill()
  } else {
    action.fail()
  }

}

// 2. When the user clicks to reject/end the call ❌
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
  
  if (_sessionID <= 0) { 
    action.fulfill()
    return
  }
  
  var _result = false
  if (***The call has been accepted and is in progress.***) {
    _result = portsipSDK.hangUp(sessionId: Int)
  } else {
    // code should have value 486
    _result = portsipSDK.rejectCall(sessionId: Int, code: Int)
  }
  
  if (_result) {
    action.fulfill()
  } else {
    action.fail()
  }
}

// 3. When the call starts, startAudio to turn on the microphone and speaker.
func provider(_: CXProvider, didActivate _: AVAudioSession) {
  portsipSDK.startAudio()
}

// 4. When the call ends, stopAudio to turn off the microphone and speaker.
func provider(_: CXProvider, didDeactivate _: AVAudioSession) {
  portsipSDK.stopAudio()
}

When the call ends, to turn off the CallKit calling screen :

func reportClosedCall() {
  
  if _callUUID != nil {
    let endCallAction = CXEndCallAction(call: _callUUID)
    
    let transaction = CXTransaction(action: endCallAction)
    
    _cxCallController = CXCallController()
    
    _cxCallController.request(transaction) { error in
      if let error = error {
        print("Error requesting transaction: \(error)")
      } else {
        print("Requested transaction successfully")
      }
    }
  }
  
}

When the user turns off the app, turns off the phone screen or kills the app, it is necessary to keep portsipSDK awake, so that the app is always ready to receive incoming calls.

func applicationDidEnterBackground(_: UIApplication) {
  portsipSDK.startKeepAwake()
}

// When the user opens the app again, stop keeping portsipSDK awake.
func applicationWillEnterForeground(_: UIApplication) {
  portsipSDK.stopKeepAwake()
}

VII. Video call

First, we create a swift file (for example: VideoCallViewController.swift) that inherits UIViewController. This file will be linked to the one ViewControllercreated through the steps 2 và 3.

class VideoCallViewController : UIViewController {
  // do something...
}

Open file Main.storyboardasInterface Builder

Create a ViewController (eg: VideoCallViewController). Click the "+" sign on the top-right corner of the xCode screen ⇒ search for "View Controller" ⇒ Double Click or Enter.

⇒ We will have a ViewController like this:

On this screen, on the Custom Classright side, we find and select the VideoCallViewController class created in step 1.

Then, in the section Identity, we Storyboard IDcan optionally set an ID for this ViewController, and check the checkbox Use Storyboard IDbelow.

⇒ Try Open file Main.storyboardas Source Code, we will see the following code:

In this ViewController, we build the necessary components (1 area to contain RemoteVideo - video from the customer, 1 area to contain LocalVideo - your own video, 1 button to End Call, buttons such as Turn On/Off Speakerphone, switch front and rear Camera, ...)

  • Create 2 connection outlets, used to attach videos to 2 areas RemoteVideo and LocalVideo. How to attach will be presented in the next step.

  • In the demo code below, there is only 1 area to contain RemoteVideo, 1 area to contain LocalVideo, 1 button to End Call.

In the file VideoCallViewController.swift(created in step 1), declare the following variables:

// used to assign portsipSDK in AppDelegate.swift to VideoCallViewController.swift => used to call Portsip API.
var portSIPSDK: PortSIPSDK!
// used to assign session Id in AppDelegate.swift to VideoCall ViewController.swift => when attaching video to RemoteVideo area, sessionId is required.
var sessionId: Int = 0

In file AppDelegate.swift:

  • Declare more variables var videoCallView: VideoCallViewController!.

  • When the extension login is successful, an event onRegisterSuccess()occurs, in this event, initialize the values ​​for VideoCallView:

func onRegisterSuccess(
  _ reason: UnsafeMutablePointer<Int8>!,
  statusCode: Int32,
  sipMessage: UnsafeMutablePointer<Int8>!
) {
  
  let storyboard = UIStoryboard(name: "Main", bundle: nil)
  videoCallView = storyboard?.instantiateViewController(withIdentifier: "VideoCallViewController") as! VideoCallViewController

  // giao diện cuộc gọi video sẽ có dạng full màn hình.
  videoCallView.modalPresentationStyle = .fullScreen

  // gán portsipSDK của AppDelegate vào portsipSDK của VideoCallViewController => 2 biến này phải bằng nhau thì gọi các api của Portsip mới không bị lỗi.
  videoCallView.portsipSDK = portsipSDK
  
}

Make a video call, call the function call()with parameter videoCall= true

_sessionID = portsipSDK.call(phoneNumber, sendSdp: true, videoCall: true)
// _sessionID is a global variable, when any call is created, there will be a session, this _sessionID variable will save the id of that session.

// phoneNumber is the phone number the user wants to call.
// sendSdp: whether to transmit to Session Description Protocol or not.
// videoCall: whether to make a video call or not.

⇒ When the called person picks up the phone: the event onInviteAnswered()occurs.

func onInviteAnswered(
  _ sessionId: Int,
  callerDisplayName: UnsafeMutablePointer<Int8>!,
  caller: UnsafeMutablePointer<Int8>!,
  calleeDisplayName: UnsafeMutablePointer<Int8>!,
  callee: UnsafeMutablePointer<Int8>!,
  audioCodecs: UnsafeMutablePointer<Int8>!,
  videoCodecs: UnsafeMutablePointer<Int8>!,
  existsAudio: Bool,
  existsVideo: Bool,
  sipMessage: UnsafeMutablePointer<Int8>!
) {
  // assign sessionId of videoCallView
  videoCallView.sessionId = sessionId

  // TODO: open the VideoCallViewController view.
// There are many ways to open a UIViewController, in this example, we use the method of 1 UIViewController opening another UIViewController using the present() function

  // RootViewController is the ViewController whose ID is assigned to the initialViewController attribute in the document tag in Main.storyboard (see image below)
  let rootController = window?.rootViewController as? RootViewController
  
  rootController.present(videoCallView, animated: true, completion: nil)
}

In file VideoCallViewController.swift:

  • Declare 2 more variables

// These are the 2 outlets we created in step 4
@IBOutlet var remoteVideo: PortSIPVideoRenderView!
@IBOutlet var localVideo: PortSIPVideoRenderView!
  • When the view is initialized for the first time, we must call the initVideoRender() function for remoteVideo and localVideo.

override func viewDidLoad() {
  localVideo.initVideoRender()
  remoteVideo.initVideoRender()
}
  • Once the view is displayed, assign the videos to localVideo and remoteVideo.

override func viewDidAppear(_ animated: Bool) {
  // localVideo - your own video
  portsipSDK.setLocalVideoWindow(localVideo)
  portsipSDK.displayLocalVideo(true, mirror: true)

  // remoteVideo - customer videos
  portsipSDK.setRemoteVideoWindow(sessionId, remoteVideoWindow: remoteVideo)
}
  • When the End Call button is clicked:

@IBAction func hangup(_ sender: Any) {
  // Tắt view videoCall đi.
  dismiss(animated: true, completion: nil)

  portsipSDK.hangUp(sessionId)
}
  • When the view is closed, remove the current video from localVideo and remoteVideo.

override func viewDidDisappear(_ animated: Bool) {
  portsipSDK.setLocalVideoWindow(nil)
  portsipSDK.displayLocalVideo(false, mirror: false)

  portsipSDK.setRemoteVideoWindow(sessionId, remoteVideoWindow: nil)
}

When a call comes in, the event onInviteIncoming()occurs

func onInviteIncoming(
  _ sessionId: Int,
  callerDisplayName: UnsafeMutablePointer<Int8>!,
  caller: UnsafeMutablePointer<Int8>!,
  calleeDisplayName: UnsafeMutablePointer<Int8>!,
  callee: UnsafeMutablePointer<Int8>!,
  audioCodecs: UnsafeMutablePointer<Int8>!,
  videoCodecs: UnsafeMutablePointer<Int8>!,
  existsAudio: Bool,
  existsVideo: Bool,
  sipMessage: UnsafeMutablePointer<Int8>!
) {
  // existsVideo to indicate whether the current call is a videoCall or not.
}

⇒ When you pick up the phone:

let code = portsipSDK.answerCall(_sessionID, videoCall: isVideoCall)
  
if code != 0 {
  return
}

if (isVideoCall) {
 // similar to making an outbound call.
  videoCallView.sessionId = _sessionID
  let rootController = window?.rootViewController as? RootViewController
  
  rootController.present(videoCallView, animated: true, completion: nil)
  }

Last updated