如何在Unity中同时加入多个声网Agora 频道

Joining Multiple Agora Channels in Unity Featured

在本教程中,你将在Unity 2019.4.1(LTS)环境中设置多个声网Agora 频道。这个项目将演示如何通过AgoraChannel对象加入多个Agora频道。在这个例子中,你会遇到加入多个频道的错综复杂的问题,并将直播发布到一个被许可的频道。

开始使用Agora

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

这个项目建立在Unity Multichannel 演示的基础上。它使用一个基本的Unity场景和 Agora

Video SDK.

用户界面设置

首先,你创建面板预制件来放置渲染到VideoSurface对象上的视频源,这些视频源是当用户加入和离开频道时由回调管理的。然后,创建启用Agora功能的按钮。

  • 加入:加入这个频道,看看其他正在发布他们的直播的人。
  • 离开:断开与频道直播的连接。
  • 发布:把你的视频直播发布到频道里,让其他人看到。
  • 取消发布:从频道中删除视频直播,但仍在可以频道中看到其他人。

在Hierarchy面板上点击右键,选择UI > Scroll View。取消选择ScrollRect组件中的Horizontal属性,并删除垂直和水平滚动条。设置RectTransform属性如下:左250,右-300,顶部-140,底部-140。确保Viewport的子RectTransform是零。将Content子项的高度设置到300。右键单击Content,选择Create Empty来创建另一个子节点,并将其命名为SpawnPoint。像这样设置位置和锚点枢轴:

接下来,创建四个按钮,命名为JoinPartyButton、LeavePartyButton、PublishToPartyButton和UnpublishFromPartyButton。将按钮中的文字更新为:“Join Party,” “Leave Party,” “Publish,” 和“Unpublish.”。

在项目窗口中,建立一个名为Prefabs的文件夹,并将整个面板拖入该文件夹以创建一个可重复使用的预制体对象。

把另一个预制体拖到场景中,制作另一个频道面板,像这样。

Agora引擎脚本

在场景中创建一个空对象,并将其命名为AgoraEngine。创建一个同名的脚本并把它附加到新创建的对象上。

using UnityEngine;
using agora_gaming_rtc;

public class AgoraEngine : MonoBehaviour
{
    public string appID;
    public static IRtcEngine mRtcEngine;

    private void Awake()
    {
        DontDestroyOnLoad(gameObject);
    }

    void Start()
    {
        if(mRtcEngine == null)
        {
            mRtcEngine = IRtcEngine.GetEngine(appID);
        }

        mRtcEngine.SetMultiChannelWant(true);

        if (mRtcEngine == null)
        {
            Debug.Log("engine is null");
            return;
        }

        mRtcEngine.EnableVideo();
        mRtcEngine.EnableVideoObserver();
    }

    private void OnApplicationQuit()
    {
        IRtcEngine.Destroy();
    }
}

请务必填写App ID!(关于如何获取App ID,请参考:声网Agora 账户注册指南

频道面板脚本

接下来,创建一个名为ChannelPanel.cs的脚本。首先,创建监听Agora引擎事件的回调。接下来,你创建代码来管理视频面板。当用户进入、退出或者发布到频道时,他们的视频面板会相应地创建和销毁。

初始化

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

public class AgoraChannelPanel : MonoBehaviour
{
    [SerializeField] private string channelName;
    [SerializeField] private string channelToken;

    [SerializeField] private Transform videoSpawnPoint;
    [SerializeField] private RectTransform panelContentWindow;
    [SerializeField] private bool isPublishing;

    private AgoraChannel newChannel;
    private List<GameObject> userVideos;

    private const float SPACE_BETWEEN_USER_VIDEOS = 150f;

    void Start()
    {
        userVideos = new List<GameObject>();
    }
}

视频表面管理

在这里,你可以编写代码来实例化和删除每个用户渲染直播视频的平面。视频平面生成一个可调整的用户界面面板,允许你滚动浏览活动的视频资料。注意一下代码,以便了解如何在用户加入和离开时适当地调整用户界面面板的长度。

void MakeImageSurface(string channelID, uint uid, bool isLocalUser = false)
    {
        if (GameObject.Find(uid.ToString()) != null)
        {
            Debug.Log("A video surface already exists with this uid: " + uid.ToString());
            return;
        }

        // Create my new image surface
        GameObject go = new GameObject();
        go.name = uid.ToString();
        RawImage userVideo = go.AddComponent<RawImage>();
        go.transform.localScale = new Vector3(1, -1, 1);

        // Child it inside the panel scroller
        if (videoSpawnPoint != null)
        {
            go.transform.SetParent(videoSpawnPoint);
        }

        // Update the layout of the panel scrollers
        panelContentWindow.sizeDelta = new Vector2(0, userVideos.Count * SPACE_BETWEEN_USER_VIDEOS);
        float spawnY = userVideos.Count * SPACE_BETWEEN_USER_VIDEOS * -1;

        userVideos.Add(go);

        go.GetComponent<RectTransform>().anchoredPosition = new Vector2(0, spawnY);

        VideoSurface videoSurface = go.AddComponent<VideoSurface>();
        if (isLocalUser == false)
        {
            videoSurface.SetForMultiChannelUser(channelID, uid);
        }
    }

    private void UpdatePlayerVideoPostions()
    {
        for (int i = 0; i < userVideos.Count; i++)
        {
            userVideos[i].GetComponent<RectTransform>().anchoredPosition = Vector2.down * SPACE_BETWEEN_USER_VIDEOS * i;
        }
    }

    private void RemoveUserVideoSurface(uint deletedUID)
    {
        foreach (GameObject user in userVideos)
        {
            if (user.name == deletedUID.ToString())
            {
                userVideos.Remove(user);
                Destroy(user);

                UpdatePlayerVideoPostions();

                Vector2 oldContent = panelContentWindow.sizeDelta;
                panelContentWindow.sizeDelta = oldContent + Vector2.down * SPACE_BETWEEN_USER_VIDEOS;
                panelContentWindow.anchoredPosition = Vector2.zero;
                break;
            }
        }        
    }

    private void OnApplicationQuit()
    {
        if(newChannel != null)
        {
            newChannel.LeaveChannel();
            newChannel.ReleaseChannel();
        }
    }

回调

回调是在某些Agora事件中自动启动的函数,比如加入和离开一个频道。使用回调函数,你可以定义自己的方法,订阅回调函数,并按照你喜欢的方式处理事件。

这个演示中的大多数回调都很标准,但请注意OnRemoteVideoStatsHandler。远程视频统计处理程序用于确定一个玩家是否正在发布他们的视频直播。

注意: 当使用AgoraChannel对象时,用户的行为就像直播频道配置文件。加入一个频道并不意味着你也会自动发布你的视频流,就像在通讯模式下一样。在这个例子中,我们必须加入一个频道并发布我们的视频直播,以便让频道中的其他人看到和听到我们。

远程视频统计根据他们在网络上发送的数据决定是否应该添加或删除视频平面。如果他们的数据比特率为零,说明他们没有发布,视频平面应该被删除。如果他们的比特率大于零,他们正在发布,他们的视频应该被显示。

#region Callbacks
    public void OnJoinChannelSuccessHandler(string channelID, uint uid, int elapsed)
    {
        Debug.Log("Join party channel success - channel: " + channelID + " uid: " + uid);
        MakeImageSurface(channelID, uid, true);
    }

    public void OnUserJoinedHandler(string channelID, uint uid, int elapsed)
    {
        Debug.Log("User: " + uid + "joined channel: + " + channelID);
    }

    private void OnLeaveHandler(string channelID, RtcStats stats)
    {
        Debug.Log("You left the party channel.");
        foreach (GameObject player in userVideos)
        {
            Destroy(player);
        }

        userVideos.Clear();
    }

    public void OnUserLeftHandler(string channelID, uint uid, USER_OFFLINE_REASON reason)
    {
        Debug.Log("User: " + uid + " left party - channel: + " + uid + "for reason: " + reason);   
        RemoveUserVideoSurface(uid);
    }
    

    private void OnRemoteVideoStatsHandler(string channelID, RemoteVideoStats remoteStats)
    {
        // If user is publishing...
        if(remoteStats.receivedBitrate > 0)
        {
            bool needsToMakeNewImageSurface = true;
            foreach (GameObject user in userVideos)
            {
                if(remoteStats.uid.ToString() == user.name)
                {
                    needsToMakeNewImageSurface = false;
                    break;
                }
            }
            // ... and their video surface isn't currently displaying ...
            if (needsToMakeNewImageSurface == true)
            {
                // ... display their feed.
                MakeImageSurface(channelID, remoteStats.uid);
            }
        }
        // If user isn't publishing ...
        else if (remoteStats.receivedBitrate == 0)
        {
            bool needsToRemoveUser = false;
            foreach (GameObject user in userVideos)
            {
                // ... but their video stream is currently displaying...
                if(remoteStats.uid.ToString() == user.name)
                {
                    needsToRemoveUser = true;
                }
            }

            if (needsToRemoveUser == true)
            {
                // ... remove their feed.
                RemoveUserVideoSurface(remoteStats.uid);
            }
        }
    }
    #endregion

按钮功能

#region Buttons
    public void Button_JoinChannel()
    {
        if (newChannel == null)
        {
            newChannel = AgoraEngine.mRtcEngine.CreateChannel(channelName);

            newChannel.ChannelOnJoinChannelSuccess = OnJoinChannelSuccessHandler;
            newChannel.ChannelOnUserJoined = OnUserJoinedHandler;
            newChannel.ChannelOnLeaveChannel = OnLeaveHandler;
            newChannel.ChannelOnUserOffLine = OnUserLeftHandler;
            newChannel.ChannelOnRemoteVideoStats = OnRemoteVideoStatsHandler;
        }

        newChannel.JoinChannel(channelToken, null, 0, new ChannelMediaOptions(true, true));
        Debug.Log("Joining channel: " + channelName);
    }

    public void Button_LeaveChannel()
    {
        if(newChannel != null)
        {
            newChannel.LeaveChannel();
            Debug.Log("Leaving channel: " + channelName);
        }
        else
        {
            Debug.LogWarning("Channel: " + channelName + " hasn't been created yet.");
        }   
    }

    public void Button_PublishToPartyChannel()
    {
        if(newChannel == null)
        {
            Debug.LogError("New channel isn't created yet.");
            return;
        }

        if(isPublishing == false)
        {
            int publishResult = newChannel.Publish();
            if(publishResult == 0)
            {
                isPublishing = true;
            }

            Debug.Log("Publishing to channel: " + channelName + " result: " + publishResult);
        }
        else
        {
            Debug.Log("Already publishing to a channel.");
        }
    }

    public void Button_CancelPublishFromChannel()
    {
        if(newChannel == null)
        {
            Debug.Log("New channel isn't created yet.");
            return;
        }

        if(isPublishing == true)
        {
            int unpublishResult = newChannel.Unpublish();
            if(unpublishResult == 0)
            {
                isPublishing = false;
            }

            Debug.Log("Unpublish from channel: " + channelName + " result: " + unpublishResult);
        }
        else
        {
            Debug.Log("Not published to any channel");
        }
    }
  #endregion

将相应的函数拖入按钮的检查器窗口中的OnClick()事件中。

我喜欢用 "Button_"作为我的按钮函数的前缀,这样我就可以在编辑器中很容易地找到这些函数,而且它们都被归类在一起,方便使用。

预制体设置

要把脚本附加到预制体上,在你的场景中选择ChannelOne预制件实例,导航到检查器面板,然后点击打开按钮。

在你打开的预制体的视图中,将脚本拖放到ChannelPanel对象中。将SpawnPoint子对象拖入Video Spawn Point字段,将Content子对象拖入Panel Content Window 字段。然后点击后退箭头返回到你的场景。

image

请注意,现在两个预制体都有脚本,并在检查器中分配了各自的子对象。Channel Name和Channel Token字段是空白的,因为每个字段都需要一个从Agora控制台生成的唯一值。

Token创建

到此为止,你应该有两个成功创建的预制体,在你的场景中没有任何编译问题,就像这样。

(如果你有问题,你可以在下面找到完整的代码和项目)。

接下来,你需要从Agora控制台生成临时Token,将它们插入每个频道预制体的相应ChannelName和ChannelToken字段,然后进行测试。

  1. 转到 console.agora.io.
  2. 导航到左侧边栏的 "项目管理 "标签。
  3. 单击 "创建 "按钮。

  1. 选择安全模式,给项目起个你想要的名字,然后点击提交按钮。

  2. 你的新项目就会像这样弹出。接下来,按下写有 "音视频临时token"的键。

  3. 输入一个频道名称。(我在我的演示中使用ChannelOne和ChannelTwo),然后点击生成临时token按钮。

  4. 从这里复制数值。

  1. 然后看这


Repeat these steps for ChannelTwo.
对频道二重复这些步骤。

结论

现在是测试的时候了! 记住,你必须发布你的直播让其他用户在频道中看到你。你只能同时向一个频道发布,但你可以同时查看许多频道的直播。

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

image

推荐阅读
相关专栏
SDK 教程
164 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。