在Unity中使用Agora的音频社交聊天直播

你好,无畏的开发者! 在本教程中,你将在Unity 2019.4.1(LTS)的3D Unity环境中设置实时音视频直播。这本质上是一个视频游戏环境,在这主播可以与同一频道中的任何人交流,并且任何观众都可以在没有实时音视频的情况下收听游戏。在加入场景时,每个玩家可以通过用户界面选择成为主播或观众,以及他们想要加入的频道。每个主播将拥有金色材料,而观众成员将有标准的维京人审美。当每个主播说话时,你将使用Agora回调来提供视觉反馈。

开始使用Agora

开始你需要一个Agora账户。如果你还没有,这里有一份建立账户的 指南

这个项目是建立在agora-unity-audio-broadcasting 演示上的。

使用agora-unity-audio-broadcasting作为一个起始模板。或者从头开始创建一个项目,并import Agora Video SDKPhoton Viking Demo

开发

创建Agora引擎

在Assets > DemoVikings > Scenes > VikingScene中,创建一个空的GameObject并命名为AgoraEngine。如果你没有位置,创建一个Scripts文件夹,并创建一个名为AgoraEngine.cs的脚本。

using UnityEngine;
using agora_gaming_rtc;

public class AgoraEngine : MonoBehaviour
{
    [Header("Agora Properties")]
    [SerializeField]
    private string appID = "";
    public static IRtcEngine mRtcEngine;

    void Start()
    {
        if(mRtcEngine != null)
        {
            IRtcEngine.Destroy();
        }

        // Initialize Agora engine
        mRtcEngine = IRtcEngine.GetEngine(appID);
    }

    // Cleaning up the Agora engine during OnApplicationQuit() is an essential part of the Agora process with Unity. 
    private void OnApplicationQuit()
    {
        TerminateAgoraEngine();
    }

    public static void TerminateAgoraEngine()
    {
        if (mRtcEngine != null)
        {
            mRtcEngine.LeaveChannel();
            mRtcEngine = null;
            IRtcEngine.Destroy();
        }
    }
}

你现在有一个可以运行的Agora引擎,你可以从你的Viking玩家对象中访问它。

但在继续之前,请在viking scene中通过右键单击Hierarchy > UI > Event System向场景添加一个EventSystem对象。

主播用户界面

帆布面板

接下来,你将创建设置频道名称和主播/观众状态所需的用户界面。在Assets > DemoVikings > Resources > Charprefab中,双击Charprefab以打开prefab视图。在层次结构中,右键单击 Charprefab parent object > UI > Panel,并命名为BroadcasterSelectionPanel。在你的Canvas object > Canvas Scalar component > UI Scale Mode > select Scale With Screen Size。这样可以在不同的设备和分辨率下保持统一的画布。在直播选择面板中,将矩形变换的属性设置为左: 250, 顶部: 200, 右: 250, 底部: 50.

按钮

接下来,你将创建按钮和文本输入栏。右键单击BroadcasterSelectionPanel > UI > Button > UI > InputField。这样做两次,以制作两个按钮。我的按钮看起来像这样:

通过在检查器中移除其名称旁边的复选标记,将BroadcasterSelectionPanel切换到关闭状态。

注意:在一个场景中有多个联网的玩家,他们所有的用户界面面板都会显示,除非切换为关闭。如果我们想只显示本地用户的用户界面,我们可以通过代码切换它。

配置文件选择脚本

现在,你将创建一个脚本,它将接收来自你刚刚创建的用户界面的输入并相应地处理这些情况。创建一个名为ProfileSelection.cs的脚本。

在ProfileSelection中,确保包括agora_gaming_rtc;,并替换为Public class ProfileSelection : MonoBehaviour改为Public class ProfileSelection : Photon.PunBehaviour。这使你能够访问PUN服务器,而且PunBehaviour使你能够访问你在其他地方看不到的PUN回调。

首先,你要为脚本创建你的属性:

using System.Collections;
using UnityEngine;
using agora_gaming_rtc;

public class ProfileSelection : Photon.PunBehaviour
{
    private bool isBroadcaster;
    private AgoraProfile agoraScript;
    private IRtcEngine agoraEngine;

    [Header("UI Elements")]
    [SerializeField]
    private GameObject BroadCastSelectionPanel;

    [Header("Broadcaster")]
    [SerializeField]
    private Material broadcasterMaterial;
    [SerializeField]
    private SkinnedMeshRenderer vikingMesh;
    
    void Start()
    {
        if(photonView.isMine)
        {
            agoraEngine = null;
            isBroadcaster = false;

            BroadCastSelectionPanel.SetActive(false);

            agoraScript = GetComponent<AgoraProfile>();
            StartCoroutine(AgoraEngineSetup());
        }
    }

接下来,你将初始化Agora引擎。引擎本身是静态的,并且在场景中有一个单独的实例。从每个玩家那里,我们寻找这个引擎并访问它。因为你是在一个联网的演示中,从玩家到引擎的同步可能会有一点延迟,所以我们创建了一个超时函数,要么在3秒内检索引擎,要么出现错误:

 IEnumerator AgoraEngineSetup()
    {
        if (photonView.isMine)
        {
            float engineTimer = 0f;
            float engineTimeout = 3f;


            while (agoraEngine == null)
            {
                agoraEngine = AgoraEngine.mRtcEngine;
                engineTimer += Time.deltaTime;

                if (engineTimer >= engineTimeout)
                {
                    Debug.LogError("InCallStats AgoraEngineSetup() Failure - No Agora Engine Found.");
                    yield break;
                }

                yield return null;
            }

            agoraEngine.SetChannelProfile(CHANNEL_PROFILE.CHANNEL_PROFILE_LIVE_BROADCASTING);
            BroadCastSelectionPanel.SetActive(true);
        }
    }

成功设置引擎后,将显示broadcastselectionpanel,其中包含按钮和一个单击时没有功能的输入字段。创建一个名为ButtonSetBroadcastState(布尔是NewStateBroadcaster)的函数。

public void ButtonSetBroadCastState(bool isNewStateBroadcaster)
{
    if (photonView.isMine)
    {
        if (isNewStateBroadcaster)
        {
            agoraEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER);
            isBroadcaster = true;

            TurnVikingGold();
        }
        else
        {
            agoraEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_AUDIENCE);
            isBroadcaster = false;
        }

        BroadCastSelectionPanel.SetActive(false);            
        agoraScript.JoinChannel();
    }
}

注意:当我创建专门用于分配给按钮的功能时,我喜欢在它们前面加上 “Button”,这样你就能在检查器中轻松找到它们。

此项目是开发环境,仅供参考,请勿直接用于生产环境 。对于在生产环境中运行的所有RTE应用程序,推荐使用Token鉴权。关于Agora平台内基于Token鉴权的更多信息,请参考 本指南

接下来,创建一个名为TurnVikingGold的函数。如果用户是主播,这个函数将通过网络发送一个“远程过程调用”(RPC)来更改必要的viking gold:

public void TurnVikingGold()
{
    if (isBroadcaster)
    {
        photonView.RPC("UpdateBroadcasterMaterial", PhotonTargets.All);
    }
}

[PunRPC]
public void UpdateBroadcasterMaterial()
{
    vikingMesh.material = broadcasterMaterial;
}

注意[PunRPC]属性,没有它,该函数将只在本地调用。也就是说,它将在你的机器上把维京人变为金色,但是当你看另一个和你一起在游戏中的客户端时,你在他们的屏幕上就不会是金色了!

最后,我们将增加一个功能,在人们开始加入实时音视频时同步主播/观众的状态。如果某人是主播,但另一个人在他们激活该状态后加入,新加入的人就不会看到主播的黄金材料。

public override void OnPhotonPlayerConnected(PhotonPlayer newPlayer)
{
    if(photonView.isMine)
    {
        base.OnPhotonPlayerConnected(newPlayer);

        TurnVikingGold();
    }
}

这就是继承Photon.PunBehaviour的关键所在,因为没有它,这个Photon事件就无法访问。

随着轮廓选择脚本的完成,现在是在检查器中分配适当元素的时候了。确保ProfileSelection被连接到你的Charprefab。把BroadcasterSelectionPanel拖到自命名的变量槽中。通过在资产窗格中右键点击Assets pane > Create > Material,创建一个名为Gold的金色材料,并将其拖入Broadcaster材质槽中。对于维京人的网格,选择Charprefab > Viking > BaseHuman。

在你的BroadcasterSelectionPanel按钮中,一个分配主播状态,另一个分配观众状态。对于每个按钮,如果OnClick()列表是空的,点击加号(+)来创建一个功能槽。把Charprefab对象拖到槽中。点击标题为 "No Function > ProfileSelection > ButtonSetBroadcastState "的下拉菜单。对于Broadcaster按钮,确保复选框被选中,Audience复选框未被选中。

注意:这个切换复选框的出现是因为我们的按钮函数有一个布尔参数,允许你从 Hierarchy 用户界面分配布尔状态。

Agora简介脚本

作为演示的最后一部分,你将创建AgoraProfile脚本,显示关于播放器的有用属性,容纳回调,并在主播说话时显示一个用户界面指示器。
创建一个名为AgoraProfile.cs的脚本,并将其附加到Charprefab上,就像你对ProfileSelection.cs所做的那样。
首先,设置驱动该脚本所需的变量,并继承Photon.PunBehaviour。

using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using agora_gaming_rtc;

public class AgoraProfile : Photon.PunBehaviour
{
    [Header("Agora Properties")]
    [SerializeField]
    private string channel;
    [SerializeField]
    private uint myUID = 0;
    [SerializeField]
    CLIENT_ROLE_TYPE myClientRole;
    [Header("UI Elements")]
    [SerializeField]
    private Text text_ChannelName;
    [SerializeField]
    private GameObject chatBubble;

    private bool isLocalPlayerTalking = false;
    [SerializeField]
    private float talkBubbleBuffer = .75f;
    private bool isSmoothingTalkBubble = false;

在启动方法中,你初始化回调和必要的变量。

void Start()
{
    if (photonView.isMine)
    {
        AgoraEngine.mRtcEngine.OnJoinChannelSuccess = OnJoinChannelSuccessHandler;
        AgoraEngine.mRtcEngine.OnUserJoined = OnUserJoinedHandler;
        AgoraEngine.mRtcEngine.OnLeaveChannel = OnLeaveChannelHandler;
        AgoraEngine.mRtcEngine.OnUserOffline = OnUserOfflineHandler;
        AgoraEngine.mRtcEngine.OnClientRoleChanged = OnClientRoleChangedHandler;
        AgoraEngine.mRtcEngine.OnVolumeIndication = OnVolumeChangedHandler;

        myClientRole = CLIENT_ROLE_TYPE.CLIENT_ROLE_AUDIENCE;

        isSmoothingTalkBubble = false;
    }
}

接下来,创建连接频道的方法:

public void JoinChannel()
{
    AgoraEngine.mRtcEngine.EnableAudioVolumeIndication(250, 3, true);

    channel = text_ChannelName.text;
    AgoraEngine.mRtcEngine.JoinChannel(text_ChannelName.text, null, 0);
}

接下来,你将创建标准的Agora回调。在这个例子中,它们是用来调试的。但是看看它们是如何工作的,这一点很重要。
注意:我把它们藏在#region里,这样你就可以把回调折叠起来。我觉得region非常有帮助。你可以自由地使用它们或不使用!

#region Agora Callbacks
// Local Client Joins Channel.
private void OnJoinChannelSuccessHandler(string channelName, uint uid, int elapsed)
{
    if (!photonView.isMine)
        return;

    Debug.Log("Local user joined - uid: " + uid);
    myUID = uid;
}

// Remote Client Joins Channel.
private void OnUserJoinedHandler(uint uid, int elapsed)
{
    if (!photonView.isMine)
        return;

    Debug.Log("Remote user joined - uid: " + uid);
}

// Local user leaves channel.
private void OnLeaveChannelHandler(RtcStats stats)
{
    if (!photonView.isMine)
        return;

    Debug.Log("Local user left channel");
}

// Remote User Leaves the Channel.
private void OnUserOfflineHandler(uint uid, USER_OFFLINE_REASON reason)
{
    if (!photonView.isMine)
        return;

    Debug.Log("Remote user left - uid: " + uid);
}

private void OnClientRoleChangedHandler(CLIENT_ROLE_TYPE oldRole, CLIENT_ROLE_TYPE newRole)
{
    myClientRole = newRole;
}
#endregion

联网的语音气泡

作为点睛之笔,你将添加一个说话的气泡,它会出现在所有主播的头上,当他们说完后就会消失。回调对说话开始和停止的时间是非常精确的,如果没有一些额外的爱护,看起来会有点不流畅。我将向你展示如何使说话的气泡平滑,使它在有人说话时看起来更自然。如果你在跟随的话,从回购中抓取语音泡泡资产,或者使用你自己的。
首先,创建两个函数,用于切换语音气泡的开启和关闭。

[PunRPC]
public void DisableSpeechBubble()
{
    chatBubble.SetActive(false);
}

[PunRPC]
public void ActivateSpeechBubble()
{
    print("my uid: " + myUID);

    chatBubble.SetActive(true);
}

接下来,创建在音量变化时启动的回调。

private void OnVolumeChangedHandler(AudioVolumeInfo[] speakers, int speakerNumber, int totalVolume)
{
    // if there is anyone speaking
    if (speakers != null)
    {
        for (int i = 0; i < speakers.Length; i++)
        {
            //Debug.Log("speaker: " + i);

            // If speaker uid == 0, that signifies you have the local player
            if (speakers[i].uid == 0)
            {
                if (speakers[i].vad == 0)
                {
                    isLocalPlayerTalking = false;
                }
                else
                {
                    isLocalPlayerTalking = true;
                    StartCoroutine(SpeechBubbleSmoothing());
                    photonView.RPC("ActivateSpeechBubble", PhotonTargets.All);
                }
            }
        }
    }
}

这将检查本地玩家是否在说话。如果他们在说话,就在他们的头顶上放置一个联网的语音气泡。你想在玩家开始说话的时候就出现说话的气泡,所以不需要进行平滑处理。

下一个函数的目的是在隐藏语音气泡之前提供一个轻微的延迟,以防止主播暂停、呼吸等。

代码的逻辑看起来像这样:

  • 当玩家停止说话时,激活talkBubbleDisableTimer。
  • 如果玩家再次开始说话,将talkBubbleDisableTimer重置为其最大值。
  • 如果玩家停止说话直到定时器达到0,则禁用说话的气泡,并退出协同程序。

注意:通过检查isSmoothingTalkBubble是否为真,该协同程序被多次调用 “屏蔽”。如果一个协同程序已经在运行,所有试图禁用通话气泡的尝试都将被否定,直到原始协同程序被停止。

private IEnumerator SpeechBubbleSmoothing()
{
    if(isSmoothingTalkBubble == true)
    {
        yield break;
    }

    isSmoothingTalkBubble = true;
    float talkBubbleDisableTimer = talkBubbleBuffer;

    while(talkBubbleDisableTimer > 0f)
    {
        if(isLocalPlayerTalking)
        {
            talkBubbleDisableTimer = talkBubbleBuffer;
        }
        talkBubbleDisableTimer -= Time.deltaTime;

        yield return null;
    }
    photonView.RPC("DisableSpeechBubble", PhotonTargets.All);
    isSmoothingTalkBubble = false;
}

注意:我把talkBubbleBuffer(起始定时器量)设置为0.75f,这是人的平均反应时间(0.75秒)。这是一个很好的默认值,对于像这样的生活质量设计技巧来说,要牢记在心,因为任何明显低于这个值的东西都可能使说话的气泡过于抖动,而任何明显高于这个值的东西都可能使说话的气泡停留时间过长。玩一玩你认为最合适的东西吧!
最后一件事。在你结束之前,你需要适当地清理一下引擎。
包括回调:

private IEnumerator OnLeftRoom()
{
    //Wait untill Photon is properly disconnected (empty room, and connected back to main server)
    while (PhotonNetwork.room != null || PhotonNetwork.connected == false)
        yield return 0;

    IRtcEngine.Destroy();
}

在Photon的演示中,有一个按钮可以让你离开大厅。这对于调试检查不同的频道或玩家状态确实很有帮助,但如果Agora引擎正在运行,它可能会导致错误。

结论

现在是测试的时候了! 如果你学到了什么,请在#unity-help-me slack频道发布,并直接给我留言,同时你也可以教教其他人

如果你想为这个项目和更广泛的Agora社区做出贡献,请随时在GitHub 上提交拉动请求,并将你的修改加入到项目中来。

获取更多文档、Demo、技术帮助

image

推荐阅读
作者信息
Sophia
TA 暂未填写个人简介
文章
10
相关专栏
SDK 教程
55 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和 Agora 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。