Flutter實現語音通話


之前忘記將代碼上傳到git,恰好只剩了當初Demo完成后的文檔,這里將文檔保存在這里,等有時間就把這個demo復現。上傳到git之后再回來更新demo地址。

該demo需接入個推SDKZego SDK

具體流程圖如下

https://www.processon.com/view/link/5e6987e4e4b0f2f3bd1965fc

語音通話demo

名詞解釋

具體名詞在對應SDK的文檔中都有提及,這里主要講述下,本demo會用到的名詞。

  • CID 即 ClientId,個推業務層中的對外用戶標識,用於標識客戶端身份,由第三方客戶端獲取並保存到第三方服務端,是個推SDK的唯一識別號,一個設備一個應用對應一個CID。
  • 拉流,表示用戶拉取別人推送出來的聲音和圖像,只拉流的話類似進直播間的用戶。
  • 推流,表示用戶將自己的手機獲取到的聲音和圖像推送出去,可以讓別人接收,只推流的話類似直播間的主播。
  • RoomID,ZegoSDK中表示房間號的意思,想要進行語音功能,就必須通話者進入同一個房間開始推流和拉流。
  • StreamID , Zego SDK中表示進入房間的成員ID,每個成員對應一個StreamID,推流時無法指定StreamID,但是拉流時可以。StreamID必須唯一,當兩個成員分配到了同一個StreamID時,最早的一個會被后分配的給頂替掉。
  • 透傳消息,個推SDK中,即自定義消息,個推只負責消息傳遞,不做任何處理,客戶端在接收到透傳消息后需要自己去處理消息的展示方式或后續動作。

實現步驟

這個demo只是完成了主要功能,詳細介紹下該如何在項目中接入相應SDK,並使用語音(視頻)通話功能。

本Demo實現的步驟如下:

  • 客戶機A,B下載軟件獲取到自己的CID,並將它發給服務器,綁定在服務器端。
  • 客戶機A向服務器發送一個帶有CID的post請求。
  • 服務接受該post請求,拿到CID,然后對照服務器的CID表。若不存在,則返回錯誤信息。
  • 如果CID表上存在該CID,服務器端接個推SDK給客戶機B發送一個透傳消息。
  • 客戶機B收到透傳消息開始處理,使用對應操作(打開來電通知界面)。
  • 客戶機B點擊接聽/拒絕,發送一個請求給服務器。
  • 如果接聽,客戶機A,B進入相同房間,開始推流並拉指定StreamID流。(即進行語音通話)

注意事項

對客戶機B來說,本demo只有接聽的功能,所以在收到服務器確認存在CID的通知后,客戶機A自動進入房間開始推流,然后客戶機B點擊接聽。客戶機A,B即可開始語音通話。

代碼實現

前言

Zego和個推都有flutter端SDK,所以直接采用。

服務器端暫時只用到了個推SDK,在個推服務器端,一開始選用了C#的,但是在運行時,缺少C#的包,去官網下載的時候,發現由於我用的是macOS無法安裝這些包,遂用pythonSDK,調用SDK的包上可能有點差異,需要去個推找一下。

服務器端也放在我的個人服務器上運行過,完成流程上是沒有問題的。不過還是遇到了一個問題,這里記錄一下。服務端發送一次透傳后再次調用該方法發送透傳信息后報錯了,可能是我使用SDK不夠規范,具體問題沒有深究下去。

我在寫demo的時候是一邊看官方文檔一邊自己調試,流程和上面寫的流程會有出路,可能會有些小的問題。還是以建議為主,代碼段是直接拷貝demo代碼,demo已經跑通,應該是沒有問題的。

客戶機A,B獲取到CID。

首先要先獲取到自己軟件的CID,這點只要接入個推SDK后,調用SDK的初始化函數就好了。他好像會把CID發送給個推服務器,然后在個推注冊了。 ### 客戶機給服務器發送帶有CID的post請求 這里需要用到dio庫。主要代碼如下:

Dio dio = new Dio();
var response = await dio.post("http://101.37.34.195:1223/", data: {"Cid":"6ce44403aba085b4018a7587ac945430"});

這里會給我的服務器發送一個請求,並夾帶想要呼叫的CId信息。

服務器收到請求並處理,不同語言表演形式不一,就不做展示了。

服務器接個推SDK給客戶機B發送透傳消息

發送透傳消息時,客戶機這里不會有顯示,建議發兩條消息,一條透傳一條通知欄消息。

服務器端代碼如下(python):

#!/usr/bin/python
# -*- coding: utf-8 -*-

from igetui.igt_message import IGtAppMessage
from igetui.template.igt_link_template import LinkTemplate
from igt_push import IGeTui
from igetui.igt_target import *
import RequestException
from igetui.template.igt_transmission_template import *

# CID = '9100a57d225596fae8f5d7d580b98a71'
CID = '6ce44403aba085b4018a7587ac945430'
CID1 = '44d512c536f2d8ea408604b45d09e858'


def pushMessageToSingle(CID1):
    #定義常量, appId、appKey、masterSecret 采用本文檔 "第二步 獲取訪問憑證 "中獲得的應用配置
    APPID = '4glq1vMvbSA0ahgSxAODb7'
    APPKEY = 'mn3MWAZ2sf7YBXGwcHWnCA'
    MASTERSECRET = 'OVu1cCwk9F9c4HDkIufG66'
    HOST = 'http://sdk.open.api.igexin.com/apiex.htm'
    push = IGeTui(HOST, APPKEY, MASTERSECRET)

    template = TransmissionTemplate()
  

    template.appId = APPID
    template.appKey = APPKEY.decode('utf-8')
    template.transmissionType = 1
    # 客戶端會收到的消息內容
    template.transmissionContent = '來電話啦1223'
    # template.isRing = True
    # template.isVibrate = True
    # template.isClearable = True

    #定義"AppMessage"類型消息對象,設置消息內容模板、發送的目標App列表、是否支持離線發送、以及離線消息有效期(單位毫秒)
    message = IGtAppMessage()
    message.data = template
    message.isOffline = True
    message.offlineExpireTime = 1000000 * 600
    message.appIdList.extend([APPID])

    target = Target()
    target.appId = APPID
    target.clientId = CID1

    try:
        ret = push.pushMessageToSingle(message, target)
        print ret
        print("13")
    except RequestException, e:
        requstId = e.getRequestId()
        ret = push.pushMessageToSingle(message, target, requstId)
        print ret

想要指定CID發送消息貌似只能用Target()。我看了c#的代碼里好像也是這樣。

服務器端收到post請求然后調用上面的方法,就可以給指定CID發送一個透傳消息,消息內容為"來電話啦1223",在客戶端處理這個消息內容就可以了。

PS:至於其他模版發送給標簽用戶與這個通話需求不符就沒有研究,如果做多方會議的話可以采用這種方式推送。可以推送給標簽用戶讓他們進入房間。

客戶機B收到透傳消息

這段在個推的SDK中也有,就是下面這個函數可以收到發送過來的消息。當這個透傳消息為服務器剛剛訂好的"來電話啦1223"時,會跳轉我提前寫好的來電通知頁面。(需要先設置好對應路由)

Getuiflut().addEventHandler(
     
      onReceiveMessageData: (Map<String, dynamic> msg) async {
        print("flutter onReceiveMessageData: $msg");
        setState(() {
          _payloadInfo = msg['payload'];
          print(_payloadInfo);
        });
        if(_payloadInfo == "來電話啦1223"){
          navigatorKey.currentState.pushNamed('/router/Phone');
        }
      },
    )

通話模塊

由於本Demo沒有做拒絕通話等處理,所以消息通知的模塊到此為止就結束了。接下來就是調用Zego SDK了。

本demo在通話模塊上沒有做太多處理,提前先寫好了加入房間,推流和拉流的過程,客戶機A,B在接聽電話時,直接調用該模塊。所以在這里就一起寫了,實際上的業務邏輯應該更復雜。

再次描述下語音通話的實現邏輯。

A進入房間開始推流,然后開始監聽房間內有沒有用戶在推流。B這時候加入房間,開始推流,然后發現A在房間內推流,直接拉A的流。此時A也進聽到B推流,直接拉B的流。至此,A和B完成了通訊。如果這個時候C進入房間他是可以直接拉到A和B的流,

所以這邊需要執行的操作就是,進入房間,推流,監聽拉流。如果不需要畫面等那么到此為止就可以進行語音通話了,如果需要畫面的話,還需要加一些代碼來顯示圖像。(如果在設置里不關閉攝像頭的話,默認是會把圖像數據一起推送出去,由於對flutter還不是很熟,這塊還不能展示)

首先是初始化:

void call(){
    print("開始打電話");
    final int appID = 272218839;
    // 填入實際從即構官網獲取到的AppSign
    final String appSign = '0xfc,0xb5,0x37,0x55,0x30,0x51,0x51,'
        '0xf9,0x6a,0x7f,0xf4,0x01,0xd6,0x9a,0x51,0xab,0xed,0x76,'
        '0xdc,0xb4,0xb4,0x35,0x7f,0x26,0x61,0x6d,0xb9,0x4b,0xbc,0xd6,0x5a,0xce';
    //測試環境
    ZegoLiveRoomPlugin.setUseTestEnv(true);
    //不開啟外部濾鏡
    ZegoLiveRoomPlugin.enableExternalVideoFilterFactory(false);
    //使用PlatformView
    ZegoLiveRoomPlugin.enablePlatformView(true);
    //初始化
    ZegoLiveRoomPlugin.initSDK(appID, appSign).then((error){
      if(error==0){
        Login_room();
        print("成功");
      }else{
        print("SDK初始化失敗");
      }
    });
  }

上述代碼只要修改appID和appSign就可以了,修改一下上述配置。

Future<void> Login_room() async {
    //登陸房間前,檢查權限
    Authorization authorization = await checkAuthorization();
    //權限為null時,表明當前運行系統無需進行動態檢查權限(android6.0以下)
    if(authorization == null){
      joinRoom();
      return;
    }//未允許授權,彈窗提示並引導用戶開啟
    if (!authorization.camera || !authorization.microphone) {

      showSettingsLink();
    }
    //授權完成,允許登錄房間
    else {
      joinRoom();
    }
  }

上述代碼是檢查是否有權限,Zogo這邊寫好了一個權限的插件,我直接拿來用了。

//在pubspec.yaml中添加
zego_permission:
    git:
      url: git://github.com/zegoim/zego-flutter-permission.git

然后下面是沒有權限申請權限的彈窗。

class Authorization {
  final bool camera;
  final bool microphone;

  Authorization(this.camera, this.microphone);
}
//彈窗顯示獲取權限
  void showSettingsLink() {
    showDialog(builder: (BuildContext context) {
      return AlertDialog(
        title: Text('提示'),
        content: Text('請到設置頁面開啟相機/麥克風權限,否則您將無法體驗音視頻功能'),
        actions: <Widget>[
          FlatButton(
            child: Text('去設置'),
            onPressed: () {
              Navigator.of(context).pop();
              ZegoPermission.openAppSettings();
            },
          ),
          FlatButton(
            child: Text('取消'),
            onPressed: () {
              Navigator.of(context).pop();
            },
          ),
        ],
      );
    });
  }

  // 請求相機、麥克風權限
  Future<Authorization> checkAuthorization() async {
    List<Permission> statusList = await ZegoPermission.getPermissions(
        <PermissionType>[PermissionType.Camera, PermissionType.MicroPhone]);

    if(statusList == null)
      return null;

    PermissionStatus cameraStatus, micStatus;
    for (var permission in statusList) {
      if (permission.permissionType == PermissionType.Camera)
        cameraStatus = permission.permissionStatus;
      if (permission.permissionType == PermissionType.MicroPhone)
        micStatus = permission.permissionStatus;
    }

    bool camReqResult = true, micReqResult = true;
    if (cameraStatus != PermissionStatus.granted ||
        micStatus != PermissionStatus.granted) {

      //不管是第一次詢問還是之前已拒絕,都直接請求權限
      if (cameraStatus != PermissionStatus.granted) {
        camReqResult = await ZegoPermission.requestPermission(
            PermissionType.Camera);
      }

      if (micStatus != PermissionStatus.granted) {
        micReqResult = await ZegoPermission.requestPermission(
            PermissionType.MicroPhone);
      }
    }

    return Authorization(camReqResult, micReqResult);
  }

然后開始進入房間

void joinRoom() {
    print("進入房間");
    String roomID = "1223";

    // 調用登錄房間之前,必須先調用setUser,設置角色,不知道干嘛的
    ZegoLiveRoomPlugin.setUser("afei1008", "阿飛");
    ZegoLiveRoomPlugin.loginRoom(roomID, 'test-room-$roomID',   ZegoRoomRole.ROOM_ROLE_ANCHOR ).then((result) {
      if(result.errorCode == 0) {
        pushStream();
        if(result.streamList.length!=0){
          for(ZegoStreamInfo stream in result.streamList){
            if(stream.streamID == "1223"){
              print("1008");
              ZegoLiveRoomPlayerPlugin.startPlayingStream(stream.streamID).then((success){
                ZegoLiveRoomPlayerPlugin.setViewMode(stream.streamID, ZegoViewMode.ZegoRendererScaleAspectFill);
                print(success);
              });
            }

          }
        }
      }else{
        print("?????????????");
      }
    });
  }

上述代碼中的result會返回當前房間內的推流的StreamId,只要在這堆StreamId中找到對應的StreamId就可以完成通話,這里我把接收的StreamId寫死了,自己推流的StreamId也寫死了,使用時這段需要修改下。

到這了就可以接收到別人推的流了,然后需要把自己的流推一下。

void pushStream() {
    print("開始推流");
    String streamID = "1008";
    ZegoLiveRoomPublisherPlugin.startPublishing(streamID, 'flutter-example', ZegoPublishFlag.ZEGO_JOIN_PUBLISH);
  }
 ```
 
 至此,本Demo的代碼部分就完結了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM