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的代码部分就完结了。
原文地址:https://www.cnblogs.com/afei123/p/12809126.html