如何将拖放式实时视频流嵌入VR(Oculus Quest)

how-to-embed-drag-and-drop-oculus

谁说远程办公就一定很无聊?!你想过和朋友聊天或在VR中看另一端世界正在发生什么吗?看看VR贸易展览会怎么样?在这里你可以直接从企业直播到VR app。你是否有兴趣使用Oculus Quest,却不确定从哪里开始?本指南能让你迅速掌握如何集成Oculus Quest并为Agora直播视频流创建一个拖放解决方案,这个方案由声网令人惊叹的亚秒延迟全球网络支持。

安装Fest

你需要:

开始使用

首先,打开Unity并创建一个名为Agora Quest Demo的空白项目。

切换构建平台

因为我们正在为Oculus Quest构建应用,所以我们可以直接将平台更改为Android,方法是“文件”>“构建设置”,然后在“平台”一栏中选择“ Android”。


确保将 纹理压缩 设置为ASTC。

安装Fest

接下来,导航到Unity Asset Store(在场景视图中,单击Asset Store选项卡)导入并下载两个asset:Oculus IntegrationAgora Video Chat SDK。Oculus负责许多有关VR项目起始的工作,而Agora在全球通信方面会给予支持。

下载Oculus Integration软件包时,系统会提示你更新Spatializer插件并重启Unity Editor。单击确定,然后在它们弹出时重新启动。

修改项目设置

首先,我们需要在播放器设置中更改一些参数,因此找到“文件”>“构建设置”>“播放器设置”。在inspector顶部,更新公司名称和产品名称。


接下来,转到“其他设置”,然后取消“Auto Graphics API”选项,来取消Vulkan。

live-realtime-blog-4-thumbnail
现在更改包名称来匹配公司和项目(你之前设置的),并将最低API等级更改为21级。


最后,鼠标向下滚动到底部的XR设置。选择“Virtual Reality Supported”,然后在“Virtual Reality SDKs”下选择“ Oculus”。

制作场景

这次,我们不制作场景,而是要使用在Assets> Oculus> VR>Scenes>Room中找到的Oculus预制场景,对其进行修改以满足我们的需求。打开房间场景,这是一个装饰明亮的空房间,并配有一些简单的手型。换句话说,对于主要用于通信的场景来说,这是一个完美的起始场景。我会移除几堵墙打造我自己的场景,然后创建一个Empty GameObject,并将它设置为我视频屏幕的位置占位符。我将此对象命名为VideoSpawn,然后将位置移动到摄像机的正前方,Z位置为1.92。

创建新素材

我们将在平面对象上显示传入的视频流。在平面上,我们将定义自己的素材。在资源文件夹中,创建一个新素材并将其命名为“ PlaneMaterial”。选择“Mobile/Unlit”作为其着色器。

inspector-mobile-unit

注意:如果跳过此步骤,那么在远程用户视频流启动时,在你的Plane对象上会显示一个粉红色的Texture。

创建一个Agora拖放预制实例

我们需要一种让我们的app与Agora进行网络对话的方法。对此最快的方法是创建一个Agora预制件,就是通过在场景中创建一个Empty GameObject并将其命名为AgoraInstance。然后使用下面的代码片段并将其添加到新的AgoraInstance中。

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

public class QuestInterface : MonoBehaviour
{
    // PLEASE KEEP THIS App ID IN SAFE PLACE
    // Get your own App ID at https://dashboard.agora.io/
    [SerializeField]
    private string appId = "AGORA-APP-ID-HERE";
    [SerializeField]
    private string roomName = "room1";

    private int numUsers = 0;
    private bool connected;


    private static QuestInterface _AgoraInstance;

    public static QuestInterface Instance { get { return _AgoraInstance; } }


    private void Awake()
    {
        if (_AgoraInstance != null && _AgoraInstance != this)
        {
            Destroy(this.gameObject);
        }
        else
        {
            _AgoraInstance = this;
        }
        InitUI();
    }

    // Use this for initialization
    private ArrayList permissionList = new ArrayList();

    // Start is called before the first frame update
    void Start()
    {
#if (UNITY_2018_3_OR_NEWER)
        permissionList.Add(Permission.Microphone);
#endif
    }

    void InitUI()
    {
        GameObject.Find("QuitButton").GetComponent<Button>().onClick.AddListener(OnQuit);
    }
    private void CheckPermission()
    {
#if (UNITY_2018_3_OR_NEWER)
        foreach (string permission in permissionList)
        {
            if (Permission.HasUserAuthorizedPermission(permission))
            {
                if (!connected)
                    onJoinRoomClicked();
            }
            else
            {
                Permission.RequestUserPermission(permission);
            }
        }
# endif
    }

    // Update is called once per frame
    void Update()
    {
#if (UNITY_2018_3_OR_NEWER)
        CheckPermission();
#endif
    }

    private void onJoinRoomClicked()
    {
        if (!connected)
        {
            connected = true;
            loadEngine();
        }
        join(roomName);
        onSceneHelloVideoLoaded();
    }



    public void onLeaveButtonClicked()
    {

        if (connected)
        {
            leave();
            unloadEngine();
            connected = false;
        }
    }

    void OnApplicationPause(bool paused)
    {
        if (paused)
        {
            if (IRtcEngine.QueryEngine() != null)
            {
                IRtcEngine.QueryEngine().DisableVideo();
            }
        }
        else
        {
            if (IRtcEngine.QueryEngine() != null)
            {
                IRtcEngine.QueryEngine().EnableVideo();
            }
        }
    }

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


    // load agora engine
    public void loadEngine()
    {
        // start sdk
        Debug.Log("initializeEngine");
        if (mRtcEngine != null)
        {
            Debug.Log("Engine exists. Please unload it first!");
            return;
        }

        // init engine
        mRtcEngine = IRtcEngine.getEngine(appId);

        // enable log
        mRtcEngine.SetLogFilter(LOG_FILTER.DEBUG | LOG_FILTER.INFO | LOG_FILTER.WARNING | LOG_FILTER.ERROR | LOG_FILTER.CRITICAL);
    }

    // unload agora engine
    public void unloadEngine()
    {
        Debug.Log("calling unloadEngine");

        // delete
        if (mRtcEngine != null)
        {
            IRtcEngine.Destroy();
            mRtcEngine = null;
        }
    }

    public void join(string channel)
    {
        Debug.Log("calling join (channel = " + channel + ")");
        if (mRtcEngine == null)
            return;

        // set callbacks (optional)
        mRtcEngine.OnJoinChannelSuccess = onJoinChannelSuccess;
        mRtcEngine.OnUserJoined = onUserJoined;
        mRtcEngine.OnUserOffline = onUserOffline;

        // enable video
        mRtcEngine.EnableVideo();

        // allow camera output callback
        mRtcEngine.EnableVideoObserver();

        // join channel
        mRtcEngine.JoinChannel(channel, null, 0);


        Debug.Log("initializeEngine done");
    }

    public void leave()
    {
        Debug.Log("calling leave");

        if (mRtcEngine == null)
            return;

        // leave channel
        mRtcEngine.LeaveChannel();
        // deregister video frame observers in native-c code
        mRtcEngine.DisableVideoObserver();

    }

    public string getSdkVersion()
    {
        return IRtcEngine.GetSdkVersion();
    }

    // accessing GameObject in Scnene1
    // set video transform delegate for statically created GameObject
    public void onSceneHelloVideoLoaded()
    {
        GameObject go = GameObject.Find("VideoSpawn");
        if (ReferenceEquals(go, null))
        {
            Debug.Log("BBBB: failed to find VideoQuad");
            return;
        }
    }

    // instance of agora engine
    public IRtcEngine mRtcEngine;

    // implement engine callbacks
    private void onJoinChannelSuccess(string channelName, uint uid, int elapsed)
    {
        Debug.Log("JoinChannelSuccessHandler: uid = " + uid);
    }

    // When a remote user joined, this delegate will be called. Typically
    // create a GameObject to render video on it
    private void onUserJoined(uint uid, int elapsed)
    {
        Debug.Log("onUserJoined: uid = " + uid);
        // this is called in main thread

        // find a game object to render video stream from 'uid'
        GameObject go = GameObject.Find(uid.ToString());
        if (!ReferenceEquals(go, null))
        {
            return; // reuse
        }

        numUsers++;
        PutUser(uid);
    }

    void PutUser(uint uid)
    { 
        // create a GameObject and assigne to this new user
        GameObject   go = GameObject.CreatePrimitive(PrimitiveType.Plane);
        if (!ReferenceEquals(go, null))
        {
            go.name = uid.ToString();

            // configure videoSurface
            VideoSurface o = go.AddComponent<VideoSurface>();
            o.SetForUser(uid);
            o.SetEnable(true);

            // Adjust view transform
            var videoQuadPos = GameObject.Find("VideoSpawn").transform.position;
            go.transform.position = videoQuadPos + new Vector3(numUsers * 0.95f, 0, 0);
            go.transform.localScale = new Vector3(0.1f, 0.5f, 0.1f);
            go.transform.Rotate(-90.0f, 1.0f, 0.0f);

            AssignShader(go);
        }
    }

    void AssignShader(GameObject go)
    {
        Material material = Resources.Load<Material>("PlaneMaterial");
        MeshRenderer mesh = go.GetComponent<MeshRenderer>();
        if (mesh != null)
        {
            mesh.material = material;
        }
    }
    // When remote user is offline, this delegate will be called. Typically
    // delete the GameObject for this user
    private void onUserOffline(uint uid, USER_OFFLINE_REASON reason)
    {
        // remove video stream
        Debug.Log("onUserOffline: uid = " + uid);
        // this is called in main thread
        GameObject go = GameObject.Find(uid.ToString());
        if (!ReferenceEquals(go, null))
        {
            Destroy(go);
        }
        numUsers--;
    }

    void OnQuit()
    {
#if UNITY_EDITOR
        UnityEditor.EditorApplication.isPlaying = false;
#else
      Application.Quit();
#endif
    }

   
}

现在,你可以将其拖动到层级结构中,以将其另存为预制件。每当你将其放入场景中时,它都会在激活预制件时自动创建一个Agora实例并加入一个场景。

在Inspector中,你可以添加Agora.io App ID和要测试的房间名称。

修改Android清单

你需要修改你的manifest文件。由于Oculus Quest没有摄像头(嗯,该demo中没有可用的摄像头),因此在文件夹Assets> Plugins> Android> AgoraRtcEngineKit.plugin的第9行中,我们将删掉摄像头的使用要求(android:name =“ android.permission.CAMERA” />)。


设备测试

现在,你需要进行测试。因为Agora不是只适用于Unity的,所以你有很多选择。你可以只创建一个样本Agora视频场景。如果你使用的是Mac,可以在此处了解如何进行操作。如果你使用的是Windows计算机,则可以在此处了解如何进行操作。但是为了方便起见,可以只使用我们的Web demo 以远程用户身份进行测试。

basic-communication

现在你已经有了一些现场视频,返回Unity编辑器并选择File> Build Settings。将修改后的房间场景拖放到“要构建的场景”一栏。注意,新的Oculus具有其构建脚本,因此你不仅要在创建Unity Editor的窗口上单击“构建并运行”,而是要转到Oculus菜单,然后选择“生成并运行”。屏幕截图如下。

OculusAgora-Room-View

短暂的加载序列后,你的app就会部署到Oculus Quest头显设备中。恭喜你!如果你想在Oculus Quest头显设备中运行app而且不需要重建,请在头显设备内部进入“Library”>“ UnkownSources”以查看你 的app。做得好!

其他资源

作者 Rick Cheng

原文链接 https://www.agora.io/en/blog/how-to-embed-drag-drop-video-streaming-vr-oculus-quest/