Unity 语音和视频通话快速解决方案——声网 SDK接入指南(Android)

一、前言

当前游戏为了增加社交互动和代入感,比如狼人杀、团队竞技游戏等,经常会产生需要实时语音和视频通话的需求。但是对于个人开发者和小团队,这种需要前后端配合,重网络的开发需求会带来很大的挑战。

为此我们需要寻找一个成熟、可靠的解决方案。每个月提供 10000 分钟免费使用时长声网 Agora成为了我的最佳选择。并且声网的 SDK(Software Development Kit) 包体积很小,运行时CPU和内存占用率低,对于移动端的游戏开发很友好。2019年7月声网正式成为了 Unity 官方认证合作伙伴,语音和视频的 SDK 也已经发布在了 Unity 资源商店中,能够非常方便的接入。

注意:语音和视频的包有冲突,不兼容(一些库和平台配置不同,可以自己手动修改),请根据需求,第四步和第五步二选一。引擎支持开关视频和声音,所以可以接入视频 SDK 包,关闭视频,仅仅使用语音通话。

二、后台创建应用

为了方便后续接入操作,这里先注册和登录到 官方后台 创建应用,获取我们之后需要的 App ID。可以根据官网的新手引导来创建应用,也可以参考如下步骤。

输入项目名称,目前暂时使用 APP ID 的鉴权模式,后续根据需要也可以在项目编辑界面切换到 Token 鉴权模式,该模式更加安全,生成 Token 程序需要搭建在服务器上,可以参考官方文档

创建成功后,我们点击 APP ID 下的显示按钮,APPID 就会复制到粘贴板。

三、获取 SDK

这里我使用的 Unity 版本为 2019.3.14,进入商店我们搜索 Agora,可以发现视频和语音两个 SDK 包。

如果打不开 Unity Store 的同学还可以从官网开发者中心下载。

四、接入 Agora Voice 语音 SDK

1. 导入工程

从 Asset Store 的我的资源(My Asset)中找到我们下载的 Agora Voice SDK For Unity ,点击 Import 导入到工程中。


导入的文件结构如下。

  • Demo:官方提供的测试语音 Demo
  • Edior:iOS 构建后处理脚本
  • Plugins:不同平台所依赖的库
  • Scripts:SDK 源码

2. 搭建测试场景

为了验证 Agora Voice 的效果,我们打开官方的 Demo,或者是自己搭建一个类似的简单场景。主要有三个按钮,分别用来测试加入频道、离开频道和静音。

3. 申请麦克风权限

在 Unity 2018.3 版本以后的新版本中,需要我们主动申请麦克风权限。

#if(UNITY_2018_3_OR_NEWER)
using UnityEngine.Android;
#endif
// ...
    private void PermissionRequest()
    {
#if (UNITY_2018_3_OR_NEWER)
			if (!Permission.HasUserAuthorizedPermission(Permission.Microphone))	// 判断是否有麦克风权限
			{
				Permission.RequestUserPermission(Permission.Microphone);	// 申请麦克风权限
			}
#endif
    }
// ...

4. 初始化 IRtcEngine

我们在调用 Agora 的接口前,需要先初始化 IRtcEngine。此时我们第二步获取的 APP ID,就在这里派上用处了。在通过 APP ID 创建 IRtcEngine 后,可以根据需求增加回调事件,这里我接入了一些比较常用的回调。

using agora_gaming_rtc;
// ...
    private IRtcEngine mRtcEngine = null;
    public const string APP_ID = "你自己的应用 APP ID";
	private void InitEngine()
    {
    	// 通过 APP ID 创建
        mRtcEngine = IRtcEngine.GetEngine(APP_ID);
        
        // 加入频道成功后的回调
        // channelName:频道名称
        // uid:用户ID(发起请求时候如果没有指定,服务器会自动分配一个)
        // elapsed:从本地用户调用 JoinChannelByKey 到该回调触发的延迟(毫秒)。
        mRtcEngine.OnJoinChannelSuccess += (string channelName, uint uid, int elapsed) =>
        {
            // ...
        };
		
        
        // 离开频道的回调
        // stats:通话统计的数据
        //		duration:通话时长
        //		txBytes:发送字节数(bytes)
        //		rxBytes:接收字节数(bytes)
        //		txKBitRate:发送码率(kbps)
        //		rxKBitRate:接收码率(kbps)
        mRtcEngine.OnLeaveChannel += (RtcStats stats) =>
        {
            string leaveChannelMessage = string.Format("onLeaveChannel callback duration {0}, tx: {1}, rx: {2}, tx kbps: {3}, rx kbps: {4}", stats.duration, stats.txBytes, stats.rxBytes, stats.txKBitRate, stats.rxKBitRate);
			// ...
        };
		
        // 用户加入回调
        // uid:新加入频道的远端用户/主播 ID
        // elapsed:从本地用户调用 JoinChannelByKey 到该回调触发的延迟(毫秒)。
        mRtcEngine.OnUserJoined += (uint uid, int elapsed) =>
        {
			// ...
        };
		
        // 用户离开回调
        // uid:离线用户或主播的用户 ID
        // reason:离线原因(主动离开、超时、直播模式身份切换)
        mRtcEngine.OnUserOffline += (uint uid, USER_OFFLINE_REASON reason) =>
        {
            string userOfflineMessage = string.Format("onUserOffline callback uid {0} {1}", uid, reason);
            Debug.Log(userOfflineMessage);
        };
		
        // 提示频道内谁在说话
        // speakers:说话人信息
        // speakerNumber:说话人数[0,3]
        // totalVolume:总音量
        mRtcEngine.OnVolumeIndication += (AudioVolumeInfo[] speakers, int speakerNumber, int totalVolume) =>
        {
            // ...
        };
	
        // 用户静音提示回调
        // uid:用户 ID
        // muted:是否静音
        mRtcEngine.OnUserMutedAudio += (uint uid, bool muted) =>
        {
            // ...
        };
		
        // 发生警告回调
        mRtcEngine.OnWarning += (int warn, string msg) =>
        {
            // ...
        };
		
        // 发生错误回调
        mRtcEngine.OnError += (int error, string msg) =>
        {
            // ...
        };
		
        // 当前通话统计回调,每两秒触发一次。
        mRtcEngine.OnRtcStats += (RtcStats stats) =>
        {
            // ...
        };
		
        // 语音路由已发生变化回调。(只在移动平台生效)
        mRtcEngine.OnAudioRouteChanged += (AUDIO_ROUTE route) =>
        {
            // ...
        };
		
        // Token 过期回调
        mRtcEngine.OnRequestToken += () =>
        {
            // ...
        };
		
        // 网络中断回调(建立成功后才会触发)
        mRtcEngine.OnConnectionInterrupted += () =>
        {
            // ...
        };
		
        // 网络连接丢失回调
        mRtcEngine.OnConnectionLost += () =>
        {
            // ...
        };
		
	    // 设置 Log 级别
        mRtcEngine.SetLogFilter(LOG_FILTER.INFO);
		// 设置为自由说话模式,常用于一对一或者群聊
        mRtcEngine.SetChannelProfile(CHANNEL_PROFILE.CHANNEL_PROFILE_COMMUNICATION);
    }
...

5. 常用 API

5.1 加入频道

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

        Debug.Log(string.Format("tap joinChannel with channel name {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);
    }

5.2 静音

    void MuteButtonTapped()
    {
        string labeltext = isMuted ? "静音" : "取消静音";
        Text label = muteButton.GetComponentInChildren<Text>();
        if (label != null)
        {
            label.text = labeltext;
        }
        isMuted = !isMuted;
        // 设置静音(停止推送本地音频)
        mRtcEngine.MuteLocalAudioStream(!isMuted);
    }

5.3 离开频道 & 销毁 IRtcEngine

    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();
            mRtcEngine = null;
        }
    }

6. 最终效果

我们可以先在编辑器上验证,能够正常运行后,将平台切换到 Android 后,直接出包就行,Agora SDK 中已经提供了 Android 中所需要的库。

最后运行在 Android 上的效果如下:

  1. 输入1234 成功加入频道,分配给我们用户 id:1186284123
  2. 有其他用户加入,用户id:2996662973
  3. 用户id:2996662973 开启静音
  4. 用户id:2996662973 离开频道
  5. 我们离开频道,显示通话统计数据
 

语音通话的 API 时序图如下:

五、接入 Agora Video 视频 SDK

1. 导入工程

如果你按照第五步导入过音频了,这里类似直接从商店导入 Agora Video SDK。要注意的是两个包的内容不同,当然两个 SDK 包的本质还是相同的,只是不同平台中的配置相关有些不同,如果同时使用,会出现问题。有能力的同学也可以尝试修改兼容,这里还是比较推荐直接删除音频 Voice 的包,再导入新的 Video 的包,这样就不用我们费尽的设置不同平台配置了。

2. 搭建测试场景

同样的我们可以直接使用Demo 中提供的场景,SceneHome 是启动场景,在最后测试的时候,注意需要修改 Build Setting 中场景列表。或者可以搭建一个简易如下的场景。

3. 申请麦克风+相机权限

与音频不同,视频需要我们添加相机权限的申请。

    private void PermissionRequest () {
#if (UNITY_2018_3_OR_NEWER)
        if (!Permission.HasUserAuthorizedPermission (Permission.Microphone)) {
            Permission.RequestUserPermission (Permission.Microphone);
        }
        // 增加相机权限
        if (!Permission.HasUserAuthorizedPermission (Permission.Camera)) {
            Permission.RequestUserPermission (Permission.Camera);
        }
#endif
    }

4. 初始化 IRtcEngine

与音频唯一不同的是需要打开视频功能。

 private void InitEngine () {
        mRtcEngine = IRtcEngine.GetEngine (APP_ID);
        // 启用视频
        mRtcEngine.EnableVideo ();
        // 允许相机回调
        mRtcEngine.EnableVideoObserver ();

     // 后续与音频相同添加回调...
 }

5. 常用 API

5.1 设置自己画面

官方已经提供好了一个用于显示的 VideoSurface 类,我们只要把它添加到需要显示的对象上即可,默认显示的即为自己相机拍摄画面。

    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);
        }
    }

5.2 设置其他用户画面

    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.SetForUser(uid);
        videoSurface.SetEnable(true);
        // 设置平面类型
        videoSurface.SetVideoSurfaceType(AgoraVideoSurfaceType.RawImage);
        // 设置画面帧率
        videoSurface.SetGameFps(30);
    }

5.3 开关视频

我们可以在应用暂停的时候,停止视频,画面将不会再更新。

    public void EnableVideo(bool pauseVideo)
    {
        if (mRtcEngine != null)
        {
            if (!pauseVideo)
            {
                // 启动视频
                mRtcEngine.EnableVideo();
            }
            else
            {
                // 关闭视频
                mRtcEngine.DisableVideo();
            }
        }
    }

6. 最终效果

最后在 Android 上运行的效果如下:

  1. 我们加入到 123 频道
  2. 给我们分配id 722438456,并显示出我们自己的画面
  3. 显示频道内另一个用户 1173951071 的画面
  4. 离开频道,视频画面停止