Linphone SDK Swift 項目移植

背景:需要將 linphone 語音相關功能移植到已有項目中,爲了儘可能少移植無用代碼,故考慮只移植 linphone sdk 和其必要的調用邏輯代碼。

思路:已有項目爲 swift 開發,首選將 linphone 移植內容封裝成靜態庫,供 swift 調用。

正式開始前要先吐槽一下 linphone 太過複雜,不經意間就會被繞進去,然後就再也出不來了。還好我只是淺嘗輒止,並未過深研究。也許深入後會有新的一片天地,那就看以後有沒有相關需要吧!

文獻

一般大家寫博文習慣將鳴謝和文獻寫在最後。一是爲了不破壞文章整體結構的流暢,二是爲了對參考文獻原作者的尊重。而這篇博文則將文獻放在了開始的地方。這是因爲,我不想贅述 linphone 是什麼,如何使用,如何移植。需要的話通過下面的鏈接,你可以獲得更多的幫助和啓示。

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 文件。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章