記 RN 項(xiàng)目中接入 VoIP 語(yǔ)音通話

最近一個(gè) RN 項(xiàng)目中需要接入 VoIP 語(yǔ)音通話功能遂蛀,雖然在學(xué)校的時(shí)候?qū)W過(guò)了 Java栈戳,做過(guò)一個(gè) Android 小項(xiàng)目,但是后面就完全沒(méi)有接觸過(guò) Android 開(kāi)發(fā)了扫沼,對(duì) Java 的了解也停留在基礎(chǔ)和一點(diǎn) Spring Boot 上崇呵,而且缤剧,iOS 開(kāi)發(fā)是完全沒(méi)有接觸過(guò)的,也就學(xué)校上課學(xué)了點(diǎn) C域慷、C++荒辕。一開(kāi)始是十分抗拒的汗销,不過(guò)想到編程總歸是相通的,邊做邊學(xué)也能搞定抵窒,于是就開(kāi)始嘗試去對(duì)接弛针,最后在經(jīng)過(guò)差不多 4 天的努力下總算的完成了。

由于對(duì)原生開(kāi)發(fā)的不熟悉估脆,項(xiàng)目中還是遇到了一些坑的钦奋,所以想把這些經(jīng)歷記錄下來(lái)。此外疙赠,完全不懂原生開(kāi)發(fā)實(shí)在是不利于做 RN 項(xiàng)目付材,所以最近打算稍微補(bǔ)一下 iOS,至少讓我能看懂別人的代碼(想干的事情又變多了圃阳,有點(diǎn)擔(dān)心貪多吃不下??)厌衔。

說(shuō)明

從功能和 UI 上來(lái)說(shuō),基本上做成和微信語(yǔ)音通話一樣的捍岳,支持主叫富寿、被叫、掛斷锣夹、靜音页徐、揚(yáng)聲器以及后臺(tái)通話。

因?yàn)椴欢_(kāi)發(fā)银萍,所以 UI 希望是能通過(guò) JS 代碼實(shí)現(xiàn)变勇,然后功能就調(diào)用封裝好的 SDK。在研究了 SDK 文檔以及 Demo 代碼之后贴唇,基本上我們要做的功能 SDK 都能提供搀绣,并且只是簡(jiǎn)單的函數(shù)調(diào)函即可,iOS 的后臺(tái)通話以及來(lái)電監(jiān)聽(tīng) SDK 也提供了戳气,那我要做的其實(shí)就只是寫好頁(yè)面链患、封裝好原生模塊然后調(diào)用就好了。

Android

Android 部分的接入相對(duì)來(lái)說(shuō)還是很容易的瓶您,畢竟 java 語(yǔ)言我是熟悉的并且了解 Android 開(kāi)發(fā)的一些基礎(chǔ)麻捻。

RN 中的 Android 原生模塊是一個(gè)繼承了 ReactContextBaseJavaModule 的類,覆蓋父類的 getName 方法返回模塊的名字呀袱,然后通過(guò) @ReactMethod 注解導(dǎo)出方法給 js 層使用芯肤,方法的返回類型必須為void。如果需要返回結(jié)果給 js压鉴,可以通過(guò)傳入 Callback 的回調(diào)函數(shù)形式或者使用 Promise 對(duì)象。通過(guò)覆蓋 getConstants 方法锻拘,可以導(dǎo)出常量給 js 使用油吭。SDK 中通話狀態(tài)的變化击蹲,可以通過(guò) RCTDeviceEventEmitter 發(fā)送事件給 js 層,然后 js 層進(jìn)行處理婉宰。封裝好的原生模塊差不多長(zhǎng)下面這樣:

public class SipModule extends ReactContextBaseJavaModule {
    private DeviceEventManagerModule.RCTDeviceEventEmitter getEventEmitter() {
        return this.getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
    }

    public SipModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    public String getName() {
        return "SipModule";
    }

    @ReactMethod
    public void registerSip(String account, String password, String addr, String port) {
        YephoneDevice.registerSip(account, password, addr + ":" + port);
        YephoneDevice.setInCallActivity(MainActivity.class);
        final DeviceEventManagerModule.RCTDeviceEventEmitter emitter = this.getEventEmitter();
        YephoneDevice.setAccountState(new YephoneManager.YephoneAccountStateChangedListener() {
            @Override
            public void state(int state, String account, String message) {
                Log.i("sip", "state:" + message);
                emitter.emit("sipStateChange", state);
            }
        });
    }
    
    ...
}

原生模塊封裝好以后歌豺,添加一個(gè) 使用了 ReactPackage 接口的 Package 類注冊(cè)該模塊,然后在 MainApplication.java 文件的 getPackages 方法中添加該 Package 即可心包。

在 JS 中类咧,通過(guò) NativeModules.SipModule 即可訪問(wèn)到添加的原生模塊,通過(guò) DeviceEventEmitter 可以注冊(cè) Android 原生端發(fā)來(lái)的事件(iOS 中稍不一樣)蟹腾。

遇到的問(wèn)題

問(wèn)題一:SDK 中要求調(diào)用 setInCallActivity 函數(shù)指定當(dāng)有來(lái)電時(shí)應(yīng)該顯示的頁(yè)面痕惋,設(shè)置好后 SDK 會(huì)在有來(lái)電時(shí)激活 App 并跳轉(zhuǎn)到該頁(yè)面,但是 RN 中只有一個(gè) MainActivicy娃殖,如果添加新的 Activity值戳,通話頁(yè)面就無(wú)法使用 js 來(lái)寫了。

這里我們希望的是在收到來(lái)電后 SDK 發(fā)出通知而不是直接進(jìn)行頁(yè)面跳轉(zhuǎn)(這確實(shí)做得太多了)炉爆,但是在于開(kāi)發(fā)商溝通后得到的結(jié)果是暫不支持堕虹。后來(lái),在與做 Android 開(kāi)發(fā)的小伙伴溝通后得知Acvicity 支持多種啟動(dòng)模式芬首,其中如果設(shè)置為 singleTop 模式赴捞,在啟動(dòng)該 Acvicity 時(shí)不會(huì)創(chuàng)建新的。這樣一來(lái)郁稍,我們只需要調(diào)用 setInCallActivity 將來(lái)電的頁(yè)面設(shè)置為 MainAcvicity 即可赦政,并且當(dāng)發(fā)生調(diào)轉(zhuǎn)時(shí)會(huì)調(diào)用 onNewIntent 生命周期函數(shù),我們可以在這里發(fā)出事件通知 js 有新的來(lái)電艺晴。

問(wèn)題二:電話接通后沒(méi)有聲音昼钻。

在完成了主叫和被叫的邏輯之后發(fā)現(xiàn)通話時(shí)沒(méi)有聲音,起初以為是設(shè)置的編碼方式不對(duì)出了問(wèn)題封寞,但是我使用的是和 Demo 中一樣的編碼然评,并且后面測(cè)試發(fā)現(xiàn) Demo 通話時(shí)也沒(méi)有聲音。原本打算聯(lián)系 SDK 開(kāi)發(fā)商解決狈究,不過(guò)后面突然想到在使用的過(guò)程中沒(méi)有提示請(qǐng)求麥克風(fēng)權(quán)限碗淌,猜測(cè)是不是與這有關(guān)。于是抖锥,在核實(shí)了沒(méi)有麥克風(fēng)權(quán)限之后亿眠,手動(dòng)打開(kāi)權(quán)限再進(jìn)行測(cè)試便可以正常通話了。最終磅废,在 js 中添加了 Android 權(quán)限申請(qǐng)的代碼纳像,解決了該問(wèn)題。

iOS

iOS 部分的接入相較于 Android 中就稍微麻煩了一點(diǎn)拯勉,首先是 Objective-C 語(yǔ)法不熟悉竟趾,然后也不太會(huì) Xcode 的使用憔购,我甚至都不知道該如何導(dǎo)入 SDK。

由于不太懂岔帽,所以我只好照著 SDK 文檔中說(shuō)的進(jìn)行操作玫鸟。首先將 SDK 目錄復(fù)制到 iOS 項(xiàng)目目錄下,然后在 Xcode 中右鍵項(xiàng)目名選擇 Add File To...犀勒,選擇剛剛復(fù)制過(guò)來(lái)的文件夾屎飘。然后在 Build Phases->Link Binary With Libraries 中添加 SDK 需要的依賴,接著在 Capabilities 中啟用 Background Modes 以支持后臺(tái)通話贾费,最后再 Build Settings 中關(guān)閉了 Bitcode(因?yàn)?SDK 不支持)钦购。其中 Bitcode 我了解到是編輯器編譯過(guò)程中的一種中間碼,先將 C铸本、OC 等高級(jí)編程語(yǔ)言轉(zhuǎn)換成 Bitcode肮雨,然后在將 Bitcode 轉(zhuǎn)換成不同 CPU 架構(gòu)上的匯編或機(jī)器碼。

根據(jù)文檔添加好 SDK 之后箱玷,我先嘗試編譯了一下怨规,然后編譯時(shí)卻報(bào)錯(cuò)了,如下圖所示锡足。

ios_build_fail.png

從錯(cuò)誤日志上可以看出是因?yàn)橛兄貜?fù)的 Symbol 導(dǎo)致的波丰,參考這篇博客使用拆分庫(kù)然后刪除對(duì)應(yīng)的 .o 文件解決了該問(wèn)題。

原生模塊

編譯通過(guò)之后舶得,就需要添加 iOS 原生模塊了掰烟。一個(gè) iOS 模板就是一個(gè)使用了 RCTBridgeModule 的 Objective-C 類。為了實(shí)現(xiàn)RCTBridgeModule沐批,類中需要包含 RCT_EXPORT_MODULE() 宏纫骑。這個(gè)宏也可以添加一個(gè)參數(shù)用來(lái)指定在 Javascript 中訪問(wèn)這個(gè)模塊的名字。如果不指定九孩,默認(rèn)就會(huì)使用這個(gè)類的名字先馆。通過(guò) RCT_EXPORT_METHOD() 宏可以聲明要給 Javascript 導(dǎo)出的方法。

// SipModule.h
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
#import "YeCallEventDelegate.h"

@interface SipModule : RCTEventEmitter <RCTBridgeModule, YeCallEventDelegate>
@property (nonatomic,strong) NSString* callID;
@end

// SipModule.m
#import "SipModule.h"
#import "YePhoneManager.h"
#import <Foundation/Foundation.h>
#import <React/RCTLog.h>

@implementation SipModule

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(registerSip:(NSString*)sipAccount sipAccPwd:(NSString*)sipAccPwd host:(NSString*)host port:(NSString*)port){
  NSString * sipProxy = [[NSString alloc] initWithFormat:@"%@:%@", host, port];
  [[YePhoneManager instance] registed:sipAccount sipAccPwd:sipAccPwd sipServerAddr:host sipProxy:sipProxy];
}

RCT_EXPORT_METHOD(answer){
  [[YePhoneManager instance] acceptCall:_callID];
}

...

多線程

參考官網(wǎng)文檔躺彬,原生模塊可以指定自己想在哪個(gè)隊(duì)列中被執(zhí)行煤墙。如果模塊需要調(diào)用一些必須在主線程才能使用的API,那應(yīng)當(dāng)這樣指定:

- (dispatch_queue_t)methodQueue
{
  return dispatch_get_main_queue();
}

類似的宪拥,如果一個(gè)操作需要花費(fèi)很長(zhǎng)時(shí)間仿野,原生模塊不應(yīng)該阻塞住,而是應(yīng)當(dāng)聲明一個(gè)用于執(zhí)行操作的獨(dú)立隊(duì)列:

- (dispatch_queue_t)methodQueue
{
  return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
}

給 Javascript 發(fā)送事件

通過(guò)繼承 RCTEventEmitter她君,實(shí)現(xiàn) suppportEvents 方法并調(diào)用 self sendEventWithName:

- (NSArray<NSString *> *)supportedEvents{
  return @[@"sipStateChange", @"sipOutRing", @"sipCallStart", @"sipCallFail", @"sipCallEnd", @"sipCallIn"];
}

- (NSString*)registrationOk:(NSString*)registrationOk{
  [self sendEventWithName:@"sipStateChange" body:@1];
  return registrationOk;
}

JavaScript代碼可以創(chuàng)建一個(gè)包含對(duì)應(yīng)模塊的 NativeEventEmitter 實(shí)例來(lái)訂閱這些事件:

import { NativeEventEmitter, DeviceEventEmitter, NativeModules } from 'react-native'

import { IS_ANDROID } from '../../utils/device';

const { SipModule } = NativeModules

export const sipEventEmitter = IS_ANDROID ? DeviceEventEmitter : new NativeEventEmitter(SipModule)

export default SipModule

總結(jié)

React Native 項(xiàng)目開(kāi)發(fā)過(guò)程中脚作,如果需要接入原生模塊或者原生 UI 組件,對(duì)于前端開(kāi)發(fā)者來(lái)說(shuō)確實(shí)不太友好缔刹,但是也并不是完全不能解決鳖枕。不過(guò)魄梯,如果能具備基本的原生開(kāi)發(fā)知識(shí),做起來(lái)也會(huì)更加事半功倍宾符,能想到更好的解決方案。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末灭翔,一起剝皮案震驚了整個(gè)濱河市魏烫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌肝箱,老刑警劉巖哄褒,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異煌张,居然都是意外死亡呐赡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門骏融,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)链嘀,“玉大人,你說(shuō)我怎么就攤上這事档玻』巢矗” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵误趴,是天一觀的道長(zhǎng)霹琼。 經(jīng)常有香客問(wèn)我,道長(zhǎng)凉当,這世上最難降的妖魔是什么枣申? 我笑而不...
    開(kāi)封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮看杭,結(jié)果婚禮上忠藤,老公的妹妹穿的比我還像新娘。我一直安慰自己泊窘,他們只是感情好熄驼,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著烘豹,像睡著了一般瓜贾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上携悯,一...
    開(kāi)封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天祭芦,我揣著相機(jī)與錄音,去河邊找鬼憔鬼。 笑死龟劲,一個(gè)胖子當(dāng)著我的面吹牛胃夏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播昌跌,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼仰禀,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了蚕愤?” 一聲冷哼從身側(cè)響起答恶,我...
    開(kāi)封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎萍诱,沒(méi)想到半個(gè)月后悬嗓,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡裕坊,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年包竹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片籍凝。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡周瞎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出静浴,到底是詐尸還是另有隱情堰氓,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布苹享,位于F島的核電站双絮,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏得问。R本人自食惡果不足惜囤攀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望宫纬。 院中可真熱鬧焚挠,春花似錦、人聲如沸漓骚。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蝌蹂。三九已至噩斟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間孤个,已是汗流浹背剃允。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人斥废。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓椒楣,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親牡肉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子捧灰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容