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.
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.
_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.
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)
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.
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)
}