@TOC

请添加图片描述


🐱‍🏍前言

  • 最近正好在网上看到声网,然后就顺道了解到了声网这个平台,发现声网的功能还真挺多呢
  • 也是一个兼容几十种平台的大公司啊,到现在才了解到,也算是相见恨晚~
  • 所以就赶紧来用 Unity 结合 声网 做一个语音聊天房!
  • 我也是第一次接入声网的SDK,可能有些地方不是很熟练,正好写一篇文章来记录学习一下~

🎂Unity 接入 声网SDK 实现 音视频通话

先简单的介绍一下声网,不了解的小伙伴可以简单认识一下~

声网 官网:https://www.agora.io/cn/community/
在这里插入图片描述
成立于 2014 年 4 月的声网Agora 是实时互动 API 平台行业开创者,是专业服务商。开发者只需简单调用 API,即可在应用内构建多种实时音视频互动场景。声网 SDK 已经赋能社交直播、在线教育、游戏电竞、IoT、AR/VR、金融、保险、医疗、企业协作等 10 余行业,共计 100 多种场景。

声网的 SDK(Software Development Kit) 包体积很小,运行时CPU和内存占用率低,对于移动端的游戏开发很友好。

2019年7月声网正式成为了 Unity 官方认证合作伙伴,语音和视频的 SDK 也已经发布在了 Unity 资源商店中,能够非常方便的接入。

下面就来一步一步搞一下这个音视频通话试试吧!


第1️⃣步,创建声网应用

首先我们需要去声网,注册登录一系列的就不说了

登录上之后来我们的控制台创建一个应用,拿到我们创建的这个应用的APPID

在这里插入图片描述

由于我们是用来测试使用的,直接选择调试模式就好啦,可以参考官网文档查看二者的区别:https://docs.agora.io/cn/Agora%20Platform/token?platform=All%20Platforms

创建成功后,点击APP ID按钮,将其复制出来,后面的代码中会用到!在这里插入图片描述


第2️⃣步,获取相应的SDK

接下来我们要下载一下音视频的SDK包,下载方式有两种

一种是在声网的官网下载对应的UnitySDK包,如下所示

在这里插入图片描述
另一种则是在Unity商店里下载,因为前面说了, 声网和Unity已经进行过合作了

@@图

如果是第一种方式在声网下载的,则是一个压缩包

里面分别有一个多平台的工具包 和 一个Unity示例工程

我们直接使用Unity示例工程即可
在这里插入图片描述
如果是从Unity商店下载的包,那就直接导入Unity工程中就好了!
在这里插入图片描述
两种方式都可以,我这里是直接在声网下载的SDK,以因为Unity商店的网速实在是太卡了,不友好~


第3️⃣步,将SDK接入Unity中

我们直接使用UnityHub把在声网下载的那个Unity示例工程给打开
在这里插入图片描述

打开之后工程视图如下所示

  • Demo:官方提供的测试语音 Demo
  • Edior:iOS 构建后处理脚本
  • Plugins:不同平台所依赖的库
  • Scripts:SDK 源码
    在这里插入图片描述
    示例工程中有两个场景,我们可以使用它的,也可以自己新建一个场景测试!
    在这里插入图片描述

第4️⃣步:搭建一个测试场景,编写测试代码

官方有一个示例场景了,我们也可以自己搭建一个场景用于测试

场景中的的按钮Text文本就不需要多说了。
两个视频聊天画面是通过RawImage显示的,一个名字为MyCamera,另一个为UserCamera,在脚本中会进行加载渲染
在这里插入图片描述

新建一个脚本VideoDemo,代码如下:

using UnityEngine;
using UnityEngine.UI;
#if(UNITY_2018_3_OR_NEWER)
using UnityEngine.Android;
#endif
using agora_gaming_rtc;

public class VideoDemo : MonoBehaviour
{
    public InputField mChannelNameInputField;//频道号
    public Text mShownMessage;//提示
    public Text versionText;//版本号
    public Button joinChannel;//加入房间
    public Button leaveChannel;//离开房间
    public Text myID;//本地UID
    public Text UserID;//远端UID

    private IRtcEngine mRtcEngine = null;

    // 输入App ID后,在App ID外删除##
    [SerializeField]
    private string AppID = "app_id";

    void Awake()
    {
        QualitySettings.vSyncCount = 0;
        Application.targetFrameRate = 30;
        CheckAppId();
    }

    // 进行初始化
    void Start()
    {
#if (UNITY_2018_3_OR_NEWER)
        // 判断是否有麦克风权限,没有权限的话主动申请权限
        if (!Permission.HasUserAuthorizedPermission(Permission.Microphone)|| !Permission.HasUserAuthorizedPermission(Permission.Camera))
        {
            Permission.RequestUserPermission(Permission.Microphone);
            Permission.RequestUserPermission(Permission.Camera);
        }

#endif
        joinChannel.onClick.AddListener(JoinChannel);
        leaveChannel.onClick.AddListener(LeaveChannel);

        mRtcEngine = IRtcEngine.GetEngine(AppID);
        versionText.GetComponent<Text>().text = "Version : " + getSdkVersion();

        // 支持视频
        mRtcEngine.EnableVideo();
        // 允许摄像机输出回调
        mRtcEngine.EnableVideoObserver();

        //加入频道成功后的回调
        mRtcEngine.OnJoinChannelSuccess += (string channelName, uint uid, int elapsed) =>
        {
            string joinSuccessMessage = string.Format("加入频道 回调 uid: {0}, channel: {1}, version: {2}", uid, channelName, getSdkVersion());
            Debug.Log(joinSuccessMessage);
            mShownMessage.GetComponent<Text>().text = (joinSuccessMessage);
            myID.text = "我的镜头\n" + uid;
            CreateMyCamera();
        };

        //离开频道回调。 
        mRtcEngine.OnLeaveChannel += (RtcStats stats) =>
        {
            string leaveChannelMessage = string.Format("离开频道回调时间 {0}, tx: {1}, rx: {2}, tx kbps: {3}, rx kbps: {4}", stats.duration, stats.txBytes, stats.rxBytes, stats.txKBitRate, stats.rxKBitRate);
            Debug.Log(leaveChannelMessage);
            mShownMessage.GetComponent<Text>().text = (leaveChannelMessage);

        };

        //远端用户加入当前频道回调。 
        mRtcEngine.OnUserJoined += (uint uid, int elapsed) =>
        {
            string userJoinedMessage = string.Format("远端用户加入当前频道回调 uid {0} {1}", uid, elapsed);
            Debug.Log(userJoinedMessage);
            mShownMessage.GetComponent<Text>().text = (userJoinedMessage);
            UserID.text = "用户镜头\n" + uid;
            CreateUserCamera(uid);
        };

        //远端用户离开当前频道回调。 
        mRtcEngine.OnUserOffline += (uint uid, USER_OFFLINE_REASON reason) =>
        {
            string userOfflineMessage = string.Format("远端用户离开当前频道回调 uid {0} {1}", uid, reason);
            Debug.Log(userOfflineMessage);
            mShownMessage.GetComponent<Text>().text = (userOfflineMessage);
        };

        //  用户音量提示回调。 
        mRtcEngine.OnVolumeIndication += (AudioVolumeInfo[] speakers, int speakerNumber, int totalVolume) =>
        {
            if (speakerNumber == 0 || speakers == null)
            {
                Debug.Log(string.Format("本地用户音量提示回调   {0}", totalVolume));
            }

            for (int idx = 0; idx < speakerNumber; idx++)
            {
                string volumeIndicationMessage = string.Format("{0} onVolumeIndication {1} {2}", speakerNumber, speakers[idx].uid, speakers[idx].volume);
                Debug.Log(volumeIndicationMessage);
            }
        };

        //用户静音提示回调
        mRtcEngine.OnUserMutedAudio += (uint uid, bool muted) =>
        {
            string userMutedMessage = string.Format("用户静音提示回调 uid {0} {1}", uid, muted);
            Debug.Log(userMutedMessage);
            mShownMessage.GetComponent<Text>().text = (userMutedMessage);
        };

        //发生警告回调
        mRtcEngine.OnWarning += (int warn, string msg) =>
        {
            string description = IRtcEngine.GetErrorDescription(warn);
            string warningMessage = string.Format("发生警告回调 {0} {1} {2}", warn, msg, description);
            Debug.Log(warningMessage);
        };

        //发生错误回调
        mRtcEngine.OnError += (int error, string msg) =>
        {
            string description = IRtcEngine.GetErrorDescription(error);
            string errorMessage = string.Format("发生错误回调 {0} {1} {2}", error, msg, description);
            Debug.Log(errorMessage);
        };

        // 当前通话统计回调,每两秒触发一次。
        mRtcEngine.OnRtcStats += (RtcStats stats) =>
        {
            string rtcStatsMessage = string.Format("onRtcStats callback duration {0}, tx: {1}, rx: {2}, tx kbps: {3}, rx kbps: {4}, tx(a) kbps: {5}, rx(a) kbps: {6} users {7}",
                stats.duration, stats.txBytes, stats.rxBytes, stats.txKBitRate, stats.rxKBitRate, stats.txAudioKBitRate, stats.rxAudioKBitRate, stats.userCount);
            //Debug.Log(rtcStatsMessage);

            int lengthOfMixingFile = mRtcEngine.GetAudioMixingDuration();
            int currentTs = mRtcEngine.GetAudioMixingCurrentPosition();

            string mixingMessage = string.Format("Mixing File Meta {0}, {1}", lengthOfMixingFile, currentTs);
            //Debug.Log(mixingMessage);
        };

        //语音路由已发生变化回调。(只在移动平台生效)
        mRtcEngine.OnAudioRouteChanged += (AUDIO_ROUTE route) =>
        {
            string routeMessage = string.Format("onAudioRouteChanged {0}", route);
            Debug.Log(routeMessage);
        };

        //Token 过期回调
        mRtcEngine.OnRequestToken += () =>
        {
            string requestKeyMessage = string.Format("OnRequestToken");
            Debug.Log(requestKeyMessage);
        };

        // 网络中断回调(建立成功后才会触发)
        mRtcEngine.OnConnectionInterrupted += () =>
        {
            string interruptedMessage = string.Format("OnConnectionInterrupted");
            Debug.Log(interruptedMessage);
        };

        // 网络连接丢失回调
        mRtcEngine.OnConnectionLost += () =>
        {
            string lostMessage = string.Format("OnConnectionLost");
            Debug.Log(lostMessage);
        };

        // 设置 Log 级别
        mRtcEngine.SetLogFilter(LOG_FILTER.INFO);

        // 1.设置为自由说话模式,常用于一对一或者群聊
        //mRtcEngine.SetChannelProfile(CHANNEL_PROFILE.CHANNEL_PROFILE_COMMUNICATION);

        //2.设置为直播模式,适用于聊天室或交互式视频流等场景。
        //mRtcEngine.SetChannelProfile (CHANNEL_PROFILE.CHANNEL_PROFILE_LIVE_BROADCASTING);

        //3.设置为游戏模式。这个配置文件使用较低比特率的编解码器,消耗更少的电力。适用于所有游戏玩家都可以自由交谈的游戏场景。
        //mRtcEngine.SetChannelProfile(CHANNEL_PROFILE.CHANNEL_PROFILE_GAME);

        //设置直播场景下的用户角色。 
        //mRtcEngine.SetClientRole (CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER);
    }

    private void CheckAppId()
    {
        Debug.Assert(AppID.Length > 10, "请先在Game Controller对象上填写你的AppId。.");
        GameObject go = GameObject.Find("AppIDText");
        if (go != null)
        {
            Text appIDText = go.GetComponent<Text>();
            if (appIDText != null)
            {
                if (string.IsNullOrEmpty(AppID))
                {
                    appIDText.text = "AppID: " + "UNDEFINED!";
                    appIDText.color = Color.red;
                }
                else
                {
                    appIDText.text = "AppID: " + AppID.Substring(0, 4) + "********" + AppID.Substring(AppID.Length - 4, 4);
                }
            }
        }
    }

    /// <summary>
    /// 加入频道
    /// </summary>
    public void JoinChannel()
    {
        // 从界面的输入框获取频道名称
        string channelName = mChannelNameInputField.text.Trim();

        Debug.Log(string.Format("从界面的输入框获取频道名称 {0}", channelName));

        if (string.IsNullOrEmpty(channelName))
        {
            return;
        }
        // 加入频道
        // channelKey: 动态秘钥,我们最开始没有选择 Token 模式,这里就可以传入 null;否则需要传入服务器生成的 Token
        // channelName: 频道名称
        // info: 开发者附带信息(非必要),不会传递给频道内其他用户
        // uid: 用户ID,0 为自动分配
        mRtcEngine.JoinChannelByKey(channelKey: null, channelName: channelName, info: "extra", uid: 0);

        //加入频道并设置发布和订阅状态。 
        //mRtcEngine.JoinChannel(channelName, "extra", 0);
        Debug.Log("频道加入方法执行完毕");
    }

    /// <summary>
    ///  离开频道
    /// </summary>
    public void LeaveChannel()
    {
        // 离开频道
        mRtcEngine.LeaveChannel();
        string channelName = mChannelNameInputField.text.Trim();
        Debug.Log(string.Format("left channel name {0}", channelName));
    }

    void OnApplicationQuit()
    {
        if (mRtcEngine != null)
        {
            // 销毁 IRtcEngine
            IRtcEngine.Destroy();
        }
    }

    /// <summary>
    /// 查询 SDK 版本号。 
    /// </summary>
    /// <returns></returns>
    public string getSdkVersion()
    {
        string ver = IRtcEngine.GetSdkVersion();
        return ver;
    }

    /// <summary>
    /// 创建本地用户显示画面
    /// </summary>
    private void CreateMyCamera()
    {
        GameObject myCamera = GameObject.Find("MyCamera");
        if (ReferenceEquals(myCamera, null))
        {
            Debug.LogError("没有找到 MyCamera 对象!");
            return;
        }
        else
        {
            myCamera.AddComponent<VideoSurface>();
            myCamera.transform.Rotate(0f, 0.0f, 180.0f);
        }
    }

    /// <summary>
    /// 创建远端用户显示画面
    /// </summary>
    /// <param name="uid"></param>
    private void CreateUserCamera(uint uid)
    {
        VideoSurface videoSurface;
        GameObject userCamera = GameObject.Find("UserCamera");
        if (ReferenceEquals(userCamera, null))
        {
            Debug.LogError("没有找到 UserCamera 对象!");
            return;
        }
        else
        {
            // 创建一个游戏对象并分配给这个新用户
            videoSurface = userCamera.AddComponent<VideoSurface>();
            userCamera.transform.Rotate(0f, 0.0f, 180.0f);

        }
        //配置videoSurface
        videoSurface.SetForUser(uid);
        videoSurface.SetEnable(true);
        videoSurface.SetVideoSurfaceType(AgoraVideoSurfaceType.RawImage);
        videoSurface.SetGameFps(30);
    }

}

第5️⃣步:视频通话API

在声网有关于视频通话的一堆API,我们可以来参考一下

视频通话API:https://docs.agora.io/cn/Video/API%20Reference/unity/index.html

这里我们只介绍几种核心的API,也是在本次实例中用到的做重点介绍,其他的可以有时间的时候自己研究一下 ~
在这里插入图片描述在这里插入图片描述在这里插入图片描述
视频通话的 API 调用时序见下图:
在这里插入图片描述


第6️⃣步:视频通话 效果测试

可以先在编辑器下看看运行效果

我这里一个是编辑器,另一个是手机进行视频通话是可以完美运行的!

效果如下所示:
请添加图片描述


🎂案例下载链接

也可以下载我的案例工程体验哦

视频通话案例下载https://download.csdn.net/download/zhangay1998/44900384


🎨总结

  • 本文简单做了一个 使用Unity实现视频通话 的案例
  • 其实非常简单,根本就没怎么动手做,因为这个视频和音频其实原理一模一样
    • 核心就是上面那张时序图一样,先进行初始化,然后加入频道聊天就可以了!
    • 自己实现一个视频通话就是这样简单,主要是我们没有对UI和逻辑进行处理
    • 只是实现了这样一个功能,在有需要的时候就可以接入相关SDK实现功能啦!

请添加图片描述