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

最近一個(gè) RN 項(xiàng)目中需要接入 VoIP 語(yǔ)音通話(huà)功能,雖然在學(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ǔ)音通話(huà)一樣的,支持主叫、被叫、掛斷、靜音、揚(yáng)聲器以及后臺(tái)通話(huà)。

因?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)通話(huà)以及來(lái)電監(jiān)聽(tīng) SDK 也提供了,那我要做的其實(shí)就只是寫(xiě)好頁(yè)面、封裝好原生模塊然后調(diào)用就好了。

Android

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

RN 中的 Android 原生模塊是一個(gè)繼承了 ReactContextBaseJavaModule 的類(lèi),覆蓋父類(lèi)的 getName 方法返回模塊的名字,然后通過(guò) @ReactMethod 注解導(dǎo)出方法給 js 層使用,方法的返回類(lèi)型必須為void。如果需要返回結(jié)果給 js,可以通過(guò)傳入 Callback 的回調(diào)函數(shù)形式或者使用 Promise 對(duì)象。通過(guò)覆蓋 getConstants 方法,可以導(dǎo)出常量給 js 使用。SDK 中通話(huà)狀態(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 類(lèi)注冊(cè)該模塊,然后在 MainApplication.java 文件的 getPackages 方法中添加該 Package 即可。

在 JS 中,通過(guò) NativeModules.SipModule 即可訪(fǎng)問(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,通話(huà)頁(yè)面就無(wú)法使用 js 來(lái)寫(xiě)了。

這里我們希望的是在收到來(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)題二:電話(huà)接通后沒(méi)有聲音。

在完成了主叫和被叫的邏輯之后發(fā)現(xiàn)通話(huà)時(shí)沒(méi)有聲音,起初以為是設(shè)置的編碼方式不對(duì)出了問(wèn)題,但是我使用的是和 Demo 中一樣的編碼,并且后面測(cè)試發(fā)現(xiàn) Demo 通話(huà)時(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è)試便可以正常通話(huà)了。最終,在 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 需要的依賴(lài),接著在 Capabilities 中啟用 Background Modes 以支持后臺(tái)通話(huà),最后再 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 類(lèi)。為了實(shí)現(xiàn)RCTBridgeModule,類(lèi)中需要包含 RCT_EXPORT_MODULE() 宏。這個(gè)宏也可以添加一個(gè)參數(shù)用來(lái)指定在 Javascript 中訪(fǎng)問(wèn)這個(gè)模塊的名字。如果不指定,默認(rèn)就會(huì)使用這個(gè)類(lèi)的名字。通過(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];
}

...

多線(xiàn)程

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

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

類(lèi)似的,如果一個(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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容