Bước 2: Khai báo permission sử dụng Microphone, Camera (để gọi video) trong file Info.plist
Bước 3: Add các thư viện, frameworks này vào TARGETS → Tên App trong XCode
Các thư viện VideoToolbox, MetalKit, GLKit, libresolv.tbd, libc++.tbd thì được XCode hỗ trợ sẵn, chỉ việc bấm dấu "+" và search tên thư viện rồi add vào.
PortSIPVoIPSDK.framework nằm trong thư mục iOSSample được giải nén từ file .zip download ở bước 1.
chú ý TARGETS không phải PROJECT
Bước 4: Trong Build Settings → Linking → Other Linker Flags: thêm vào giá trị -ObjC
Những ví dụ từ mục II trở đi đều áp dụng trong chỉ 1 file duy nhất: AppDelegate.swift
II. Khởi tạo SDK
Bước 1: Khai báo biến global let portsipSDK: PortSIPSDK! = PortSIPSDK() . Dùng để gọi các API của PortsipSDK.
Bước 2:Mở file Runner-Bridging-Header.h, import thêm dòng này vào #import <PortSIPVoIPSDK/PortSIPVoIPSDK.h>
Bước 3: Phải implement interface: **PortSIPEventDelegate .** trong Class AppDelegate
func initPortsipSDK() {portsipSDK.unInitialize()portsipSDK.delegate = selfportsipSDK.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)// những cấu hình liên quan đến AudioportsipSDK.addAudioCodec(AUDIOCODEC_OPUS)portsipSDK.addAudioCodec(AUDIOCODEC_G729)portsipSDK.addAudioCodec(AUDIOCODEC_PCMA)portsipSDK.addAudioCodec(AUDIOCODEC_PCMU)portsipSDK.setAudioSamples(20, maxPtime: 60)// những cấu hình liên quan đến VideoportsipSDK.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")}
Hàm này nên được gọi chung lúc với đăng nhập máy nhánh.
Nên có 1 biến portsipRegistered dùng để lưu trạng thái đã register với portsip thành công hay chưa. Nếu đã register thành công thì chỉ cần gọi API refreshRegistration() , không cần qua 2 bước initPortsipSDK() và registerPortsip() bên trên.
portsipSDK.refreshRegistration(0)
Nếu muốn register lại từ đầu thì phải Khởi tạo SDK lại như mục II.
⇒ Khi đăng nhập thành công: sự kiện onRegisterSuccess() xảy ra.
⇒ Ngược lại, khi không thành công thì sự kiện onRegisterFailure() xảy ra.
_sessionID =portsipSDK.call(callee: String, sendSdp: Bool, videoCall: Bool)// _sessionID là 1 biến global, khi một cuộc gọi bất kì được tạo ra thì sẽ có 1 session, biến _sessionID này sẽ lưu lại id của session đó.// callee là số điện thoại user muốn gọi đến.// sendSdp: có truyền lên Session Description Protocol hay không. Thường sẽ để bằng true// videoCall: có thực hiện video call hay không.
voipRegistry: PKPushRegistry! = PKPushRegistry(queue: DispatchQueue.main) . Dùng để khai báo về việc nhận notification từ Portsip Mobile Push.
_VoIPPushToken: String = "" . Token dùng để nhận notification VoIP.
_APNsPushToken: String = "" . Token dùng để nhận notification chung.
_cxProvider: CXProvider! . Dùng để hiển thị popup cuộc gọi đến
_cxCallController: CXCallController = CXCallController() . Dùng để thao tác với các cuộc gọi CallKit như ngắt máy, bắt máy, ...
class AppDelegate implement thêm các interface sau: UIApplicationDelegate, PKPushRegistryDelegate, CXProviderDelegate
class AppDelegate import thêm các service sau: PushKit, UserNotifications, CallKit
Phải có tối thiểu 2 hàm application() ở file AppDelegate.swift
1 hàm application mặc định. Tại đây, enable CallKit (để có thể nhận cuộc gọi, nghe-gọi khi ở background mode), PushNotification (để sử dụng Portsip Mobile Push), ForceBackground (để app chạy ở chế độ background)
1 hàm application dùng để lấy device token dùng cho việc nhận notification
func application( _ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {let tokenParts =deviceToken.map { data in String(format:"%02.2hhx", data) } _APNsPushToken =tokenParts.joined()updatePushStatusToSipServer() **// khai báo Header dùng để gọi API Register với Portsip. Sẽ nói ở bước 9.**}
Phải có tối thiểu 2 hàm pushRegistry() ở file AppDelegate.swift
1 hàm pushRegistry dùng để lấy token dùng cho việc nhận notification VoIP
func pushRegistry(_: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for _: PKPushType) {let tokenParts =pushCredentials.token.map { data in String(format:"%02.2hhx", data) } _VoIPPushToken =tokenParts.joined()updatePushStatusToSipServer() **// khai báo Header dùng để gọi API Register với Portsip. Sẽ nói ở bước 9.**}
1 hàm pushRegistry dùng để hứng notification VoIP từ Portsip Mobile Push. Khi có cuộc gọi đến, thì portsip sẽ bắn notification về cho thiết bị của người dùng.
func pushRegistry(_: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for _: PKPushType) {**// Nếu không muốn nhận notification VoIP khi app đang active thì check điều kiện này.**ifUIApplication.shared.applicationState == .active {return }processPushMessageFromPortPBX(payload.dictionaryPayload, completion: {}) **// Dùng CallKit để thông báo cuộc gọi đến. Sẽ nói ở bước 10.**}
Khai báo Header với _VoIPPushToken và _APNsPushToken trước khi gọi API Register hay RefreshRegistration của portsipSDK (mục II bước 1).
Dùng CallKit để thông báo cuộc gọi đến, có thể hiện thực như sau:
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 = dictionaryPayloadlet pushId = dictionaryPayload["x-push-id"]// _callUUID là 1 biến global để dùng trong việc thông báo cuộc gọi đến bằng CallKitif 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 = handleupdate.supportsDTMF =falseupdate.hasVideo =falseupdate.supportsGrouping =falseupdate.supportsUngrouping =falseupdate.supportsHolding =falselet infoDic =Bundle.main.infoDictionary!let localizedName = infoDic["CFBundleName"] as! Stringlet providerConfiguration =CXProviderConfiguration(localizedName: localizedName)providerConfiguration.supportsVideo =trueproviderConfiguration.maximumCallsPerCallGroup =1providerConfiguration.supportedHandleTypes = [.phoneNumber] _cxProvider =CXProvider(configuration: providerConfiguration)_cxProvider.setDelegate(self, queue: DispatchQueue.main)_cxProvider.reportNewIncomingCall(with: _callUUID, update: update, completion: {})}
Một số thao tác user có thể thực hiện khi có cuộc gọi từ CallKit: Chấp nhận (bắt máy), Từ chối, Ngắt máy (sau khi đã chấp nhận), ...
// 1. Khi người dùng bấm vào nút Chấp nhận cuộc gọi ✅func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {ifportsipSDK.answerCall(sessionId: Int, videoCall: Bool) {action.fulfill() } else {action.fail() }}// 2. Khi người dùng bấm từ chối/kết thúc cuộc gọi ❌func provider(_ provider: CXProvider, perform action: CXEndCallAction) {if (_sessionID <=0) { action.fulfill()return }var _result =falseif (***cuộc gọi đã được chấp nhận và đang diễn ra***) { _result =portsipSDK.hangUp(sessionId: Int) } else {// code nên có giá trị là 486 _result =portsipSDK.rejectCall(sessionId: Int, code: Int) }if (_result) {action.fulfill() } else {action.fail() }}// 3. Khi cuộc gọi bắt đầu, startAudio để mở microphone và speaker.func provider(_: CXProvider, didActivate _: AVAudioSession) {portsipSDK.startAudio()}// 4. Khi cuộc gọi kết thúc, stopAudio để tắt microphone và speaker.func provider(_: CXProvider, didDeactivate _: AVAudioSession) {portsipSDK.stopAudio()}
Khi cuộc gọi kết thúc, để tắt màn hình gọi điện của CallKit đi:
Khi người dùng tắt app, tắt màn hình điện thoại hoặc kill app thì cần phải giữ cho portsipSDK awake, để ứng dụng luôn sẵn sàng nhận cuộc gọi đến.
func applicationDidEnterBackground(_: UIApplication) {portsipSDK.startKeepAwake()}// Khi người dùng mở app lại thì ngưng việc giữ cho portsipSDK awake đi.func applicationWillEnterForeground(_: UIApplication) {portsipSDK.stopKeepAwake()}
VII. Cuộc gọi video
Đầu tiên, ta tạo 1 file swift (ví dụ: VideoCallViewController.swift) kế thừa UIViewController. File này sẽ được link với 1 ViewController được tạo ra qua các bước 2 và 3.
classVideoCallViewController:UIViewController {// do something...}
Open file Main.storyboard dưới dạng Interface Builder
Tạo 1 ViewController (ví dụ: VideoCallViewController). Bấm dấu "+" phía góc phải-trên của màn hình xCode ⇒ search từ "View Controller" ⇒ Double Click hoặc Enter.
⇒ Ta sẽ có 1 ViewController như thế này:
Tại màn hình này, ở phần Custom Class ở bên phải, ta tìm và chọn class VideoCallViewController được tạo trước ở bước 1.
Sau đó, ở phần Identity, mục Storyboard ID ta tuỳ ý đặt cho ViewController này 1 ID, đồng thời check vào checkbox Use Storyboard ID bên dưới.
⇒ Thử Open file Main.storyboard dưới dạng Source Code, ta sẽ thấy đoạn code như sau:
Trong ViewController này, ta tiến hành dựng các component cần thiết (1 vùng để chứa RemoteVideo - tức video từ phía khách hàng, 1 vùng để chứa LocalVideo - tức video của chính bạn, 1 button để Kết thúc cuộc gọi, các button như Bật/Tắt loa ngoài, chuyển đổi Camera trước và sau, ...)
Tạo 2 connection outlets, dùng để gắn video vào 2 vùng RemoteVideo và LocalVideo. Cách gắn sẽ trình bày ở bước sau.
Ở đoạn code demo bên dưới, chỉ bao gồm 1 vùng để chứa RemoteVideo, 1 vùng để chứa LocalVideo, 1 button để Kết thúc cuộc gọi.
Trong file VideoCallViewController.swift (đã tạo ở bước 1), khai báo các biến sau:
// dùng để gán portsipSDK ở AppDelegate.swift vào VideoCallViewController.swift => dùng để gọi API Portsip.var portSIPSDK: PortSIPSDK!// dùng để gán sessionId ở AppDelegate.swift vào VideoCallViewController.swift => khi gắn video vào vùng RemoteVideo thì cần có sessionId.var sessionId: Int=0
Trong file AppDelegate.swift:
Khai báo thêm biến var videoCallView: VideoCallViewController!.
Khi đăng nhập máy nhánh thành công, sự kiện onRegisterSuccess() xảy ra, trong sự kiện này, khởi tạo các giá trị cho VideoCallView:
funconRegisterSuccess(_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}
Thực hiện cuộc gọi video, gọi hàm call() với tham số videoCall = true
_sessionID =portsipSDK.call(phoneNumber, sendSdp: true, videoCall: true)// _sessionID là 1 biến global, khi một cuộc gọi bất kì được tạo ra thì sẽ có 1 session, biến _sessionID này sẽ lưu lại id của session đó.// phoneNumber là số điện thoại user muốn gọi đến.// sendSdp: có truyền lên Session Description Protocol hay không.// videoCall: có thực hiện video call hay không.
⇒ Khi người được gọi nhấc máy: sự kiện onInviteAnswered() xảy ra.
funconInviteAnswered(_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>!) {// gán sessionId của videoCallView videoCallView.sessionId = sessionId// TODO: mở view VideoCallViewController lên. // Có nhiều cách để mở một UIViewController lên, trong ví dụ này, ta dùng cách 1 UIViewController mở 1 UIViewController khác lên bằng hàm present()// RootViewController là ViewController có ID được gán vào attribute initialViewController ở thẻ document trong Main.storyboard (xem hình bên dưới)let rootController = window?.rootViewController as? RootViewController rootController.present(videoCallView, animated:true, completion:nil)}
Trong file VideoCallViewController.swift:
Khai báo thêm 2 biến
// Đây là 2 outlet ta tạo ra ở bước 4@IBOutletvar remoteVideo: PortSIPVideoRenderView!@IBOutletvar localVideo: PortSIPVideoRenderView!
Khi view được init lần đầu tiên, ta phải gọi hàm initVideoRender() cho remoteVideo và localVideo.
Khi view đã hiển thị, gắn video cho localVideo và remoteVideo.
overridefuncviewDidAppear(_animated: Bool) {// localVideo - video của chính bạn portsipSDK.setLocalVideoWindow(localVideo) portsipSDK.displayLocalVideo(true, mirror:true)// remoteVideo - video của khách hàng portsipSDK.setRemoteVideoWindow(sessionId, remoteVideoWindow: remoteVideo)}
Khi có cuộc gọi đến, sự kiện onInviteIncoming() xảy ra
funconInviteIncoming(_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 để cho biết cuộc gọi hiện tại có phải là videoCall hay không.}
⇒ Khi bạn nhấc máy:
let code = portsipSDK.answerCall(_sessionID, videoCall: isVideoCall)if code !=0 {return}if(isVideoCall) {// tương tự như khi thực hiện cuộc gọi ra. videoCallView.sessionId = _sessionIDlet rootController = window?.rootViewController as? RootViewController rootController.present(videoCallView, animated:true, completion:nil)}