背景:需要將 linphone 語音相關功能移植到已有項目中,爲了儘可能少移植無用代碼,故考慮只移植 linphone sdk 和其必要的調用邏輯代碼。
思路:已有項目爲 swift 開發,首選將 linphone 移植內容封裝成靜態庫,供 swift 調用。
正式開始前要先吐槽一下 linphone 太過複雜,不經意間就會被繞進去,然後就再也出不來了。還好我只是淺嘗輒止,並未過深研究。也許深入後會有新的一片天地,那就看以後有沒有相關需要吧!
文獻
一般大家寫博文習慣將鳴謝和文獻寫在最後。一是爲了不破壞文章整體結構的流暢,二是爲了對參考文獻原作者的尊重。而這篇博文則將文獻放在了開始的地方。這是因爲,我不想贅述 linphone 是什麼,如何使用,如何移植。需要的話通過下面的鏈接,你可以獲得更多的幫助和啓示。
linphone 相關博文:
- 官網:linphone.org
- linphone SDK 官方下載:snapshots-ios
- Linphone-iOS-移植
- 怎樣將Linphone移植到自己的項目 值得推薦
我的封裝
linphone SDK 封裝靜態庫
通過 xcode 的 File -> New -> Project -> 選擇 IOS Framework & Library 下的 Cocoa Touch Static Library 創建一個新的項目。當然這個項目可以是獨立的,也可以和你原有項目在同一個 workspace,我的項目是後者。
linphone 靜態庫工程結構如下圖:
這裏沒什麼好說的,基本結構和方法與參考博文 怎樣將Linphone移植到自己的項目 中的開源項目基本一致。但由於我的已有項目是 Swift 開發的,並不想使用除 linphone sdk 必須的調用文件以外的其他文件。所以並沒有使用 ColorSpaceUtilites,UCSIPCCSDKLog 和 UCSIPCCDelegate(我使用系統通知進行狀態更新)。
另外代碼中不同的地方有:
// LinphoneSDK 等價於 UCSIPCCManager
// LinphoneSDK.h 添加了micro 和 pauseCall 的操作
@property (nonatomic, assign) BOOL microEnabled; // 是否打開揚聲器
@property (nonatomic, assign) BOOL pauseCallEnabled; // 是否打開暫停開關
// LinphoneSDK.m
/**
@author robertzhang, 16-08-02
麥克風狀態
*/
- (BOOL)microEnabled {
return linphone_core_is_mic_muted([LinphoneManager getLc]) == false;
}
- (void)setMicroEnabled:(BOOL)microEnabled {
linphone_core_mute_mic([LinphoneManager getLc], microEnabled);
}
/**
@author robertzhang, 16-08-02
通話暫停鍵狀態
*/
- (BOOL)pauseCallEnabled {
bool ret = false;
LinphoneCall *c = [self getCall];
if (c != nil) {
LinphoneCallState state = linphone_call_get_state(c);
ret = (state == LinphoneCallPaused || state == LinphoneCallPausing);
}
return ret;
}
- (void)setPauseCallEnabled:(BOOL)pauseCallEnabled {
LinphoneCall *currentCall = [self getCall];
if (currentCall != nil) {
if (pauseCallEnabled) {
linphone_core_pause_call(LC, currentCall); // 暫停
} else {
linphone_core_resume_call(LC, currentCall); // 取消暫停
}
}
}
- (LinphoneCall *)getCall {
LinphoneCall *currentCall = linphone_core_get_current_call(LC);
if (currentCall == nil && linphone_core_get_calls_nb(LC) == 1) {
currentCall = (LinphoneCall *)linphone_core_get_calls(LC)->data;
}
return currentCall;
}
另外,因爲靜態庫無法打包進資源文件,所以音頻文件需要通過bundle進行管理和使用。LinphoneManager 也需要做相應的修改
// LinphoneManager.m 加載音頻的地方修改如下
NSBundle *libs = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"Resources" ofType:@"bundle"]];
const char* lRing = [[libs pathForResource:@"ring" ofType:@"wav"] cStringUsingEncoding:[NSString defaultCStringEncoding]];
linphone_core_set_ring(theLinphoneCore, lRing);
const char* lRingBack = [[libs pathForResource:@"ringback" ofType:@"wav"] cStringUsingEncoding:[NSString defaultCStringEncoding]];
linphone_core_set_ringback(theLinphoneCore, lRingBack);
const char* lPlay = [[libs pathForResource:@"hold" ofType:@"wav"] cStringUsingEncoding:[NSString defaultCStringEncoding]];
linphone_core_set_play_file(theLinphoneCore, lPlay);
靜態庫的創建需要十分小心配置文件的設置,很容易因此被坑到。我就是因爲沒有在 Compile Sources 中將新添加的文件引入,導致報了很多莫名的錯誤。當然這和我自己粗心有很大關係。
Swift 使用靜態庫
靜態庫引入已有工程中如果出現 xcode 提示 linphoneSDK.a 文件不存在等這類問題,那是因爲對於新 clone 的代碼,linphoneSDK.a 的引用失效所致。如果 linphoneSDK 工程和已有工程在同一個 workspace,只需要先編譯一下 linphoneSDK 工程,然後將生成的 linphoneSDK.a 文件以及必要的 include 文件重新引入即可。具體的步驟可參看:JZPhone 的 README
以上是我在 Swift 項目中對 linphoneSDK 的一層封裝
/*
這裏用 Delegate 是爲了 VOIP 接口的易擴展,VoipDelegate 相當於一個適配器。
當需要接入新的 VOIP 組件,只需要創建一個接口類,並實現 VoipDelegate 協議。
eg: 創建 linphoneSDK.swift ,繼承VoipDelegate。通過實現協議中的方法,調用linphone SDK動態庫中的 OC 代碼完成功能。
*/
/// Voip 組件協議
protocol VoipDelegate {
/// 初始化 voip 組件
func initVOIP()
/// 登陸
func login(name: String, password: String)
/// 登出
func logout()
/// 撥號
func onCall(address: String, displayName: String? , transfer: Bool?)
/// 接電話
func acceptCall()
/// 掛斷
func hangUpCall()
/// 獲取通話時長
func getCallDuration() -> Int
/// 獲取對方號碼
func getRemoteAddress() -> String?
/// 獲取對方暱稱
func getRemoteDisplayName() -> String?
/// 打開、關閉揚聲器
func switchSpeaker()
/// 查看揚聲器狀態
func isSpeakerOpened() -> Bool
/// 打開、關閉麥克風
func switchMicro()
/// 查看麥克風狀態
func isMicroOpened() -> Bool
/// 通話掛起
func switchPauseCall()
/// 通話掛起狀態
func isPauseCall() -> Bool
}
// CallHelper.swift
class CallHelper {
...
/// 通過協議調用相關操作
static var voipDelegate: VoipDelegate?
...
}
實現 VoipDelegate 協議接入 linphoneSDK
class LinphoneHelper: VoipDelegate {
let domain = "xxx.xxx.xxx.xx"
let port = "xxxx"
let transport = "UDP"
class var sharedInstance: LinphoneHelper {
struct Static {
static let instance: LinphoneHelper = LinphoneHelper()
}
return Static.instance
}
// MARK: - Implements VoipDelegate
/// 初始化 voip 組件
func initVOIP() {
LinphoneSDK.instance().startLPhone()
}
/// 登陸
func login(name: String, password: String) {
LinphoneSDK.instance().addProxyConfig(name, password: password, displayName: name, domain: domain, port: port, withTransport: transport)
}
/// 登出
func logout() {
LinphoneSDK.instance().removeAccount()
}
/// 撥號
func onCall(address: String, displayName: String? , transfer: Bool?) {
LinphoneSDK.instance().call(address, displayName: address, transfer: false)
}
/// 接電話
func acceptCall() {}
func acceptCall(notif: NSNotification) {
LinphoneSDK.instance().acceptCall(notif)
}
/// 掛斷
func hangUpCall() {
LinphoneSDK.instance().hangUpCall()
}
/// 獲取通話時長
func getCallDuration() -> Int {
let duration = Int(LinphoneSDK.instance().getCallDuration())
return duration
}
/// 獲取對方號碼
func getRemoteAddress() -> String? {
let address = LinphoneSDK.instance().getRemoteAddress()
return address
}
/// 獲取對方暱稱
func getRemoteDisplayName() -> String? {
let displayname = LinphoneSDK.instance().getRemoteDisplayName()
return displayname
}
/// 打開、關閉揚聲器
func switchSpeaker() {
LinphoneSDK.instance().speakerEnabled = LinphoneSDK.instance().speakerEnabled ? false : true
}
/// 查看揚聲器狀態
func isSpeakerOpened() -> Bool {
return LinphoneSDK.instance().speakerEnabled
}
/// 打開、關閉麥克風
func switchMicro() {
LinphoneSDK.instance().microEnabled = LinphoneSDK.instance().microEnabled ? true : false
}
/// 查看麥克風狀態
func isMicroOpened() -> Bool {
return LinphoneSDK.instance().microEnabled
}
/// 通話掛起
func switchPauseCall() {
LinphoneSDK.instance().pauseCallEnabled = LinphoneSDK.instance().pauseCallEnabled ? false : true
}
/// 通話掛起狀態
func isPauseCall() -> Bool {
return LinphoneSDK.instance().pauseCallEnabled
}
/// 環境是否準備成功
func isSDKReady() -> Bool {
return LinphoneSDK.instance().isLPReady
}
/// 獲取用戶名或地址(sip賬號)
func getName() -> String{
if let name = LinphoneHelper.sharedInstance.getRemoteDisplayName() where (name as NSString).length > 0 {
return name
} else {
let address = LinphoneHelper.sharedInstance.getRemoteAddress()! as NSString
return address.substringToIndex(address.rangeOfString("@").location)
}
}
}
linphoneSDK 靜態庫是使用 OC 編寫的,Swift 要調用就必須加入橋接,具體辦法請自行查閱在此不一贅述。到這裏就完成了封裝工作。按照我個人的習慣,爲了不破壞已有代碼的邏輯,也爲了讓兩個關係不大的模塊儘可能的低耦合,做一層層的封裝還是有必要的。
細心的你一定在上面發現了
static var voipDelegate: VoipDelegate?
這段代碼她是做什麼用的呢?沒錯,她就是我們解耦的關鍵。
static var voipDelegate: VoipDelegate? = LinphoneHelper.sharedInstance
我們只需要替換實現 voipDelegate 協議的實例對象,就可以自如的切換不同的 VOIP 組件,是不是很神奇呢?剩下的就不用我多說了,在你需要的地方盡情使用 voipDelegate 來調用 VOIP 的功能吧。
說明
以上開發環境如下:
swift: 2.2
xcode: 7.3
linphone 版本: 老的版本
替換到最新版本 linphone sdk 需要相應的替換 linphoneManager 文件。