谁说远程办公就一定很无聊?!你想过和朋友聊天或在VR中看另一端世界正在发生什么吗?看看VR贸易展览会怎么样?在这里你可以直接从企业直播到VR app。你是否有兴趣使用Oculus Quest,却不确定从哪里开始?本指南能让你迅速掌握如何集成Oculus Quest并为Agora直播视频流创建一个拖放解决方案,这个方案由声网令人惊叹的亚秒延迟全球网络支持。
安装Fest
你需要:
- Unity编辑器 (Android支持)
- Agora.io开发者帐户
- Oculus开发者帐户
- Oculus Quest
开始使用
首先,打开Unity并创建一个名为Agora Quest Demo的空白项目。
切换构建平台
因为我们正在为Oculus Quest构建应用,所以我们可以直接将平台更改为Android,方法是“文件”>“构建设置”,然后在“平台”一栏中选择“ Android”。
确保将 纹理压缩 设置为ASTC。
安装Fest
接下来,导航到Unity Asset Store(在场景视图中,单击Asset Store选项卡)导入并下载两个asset:Oculus Integration和Agora Video Chat SDK。Oculus负责许多有关VR项目起始的工作,而Agora在全球通信方面会给予支持。
下载Oculus Integration软件包时,系统会提示你更新Spatializer插件并重启Unity Editor。单击确定,然后在它们弹出时重新启动。
修改项目设置
首先,我们需要在播放器设置中更改一些参数,因此找到“文件”>“构建设置”>“播放器设置”。在inspector顶部,更新公司名称和产品名称。
接下来,转到“其他设置”,然后取消“Auto Graphics API”选项,来取消Vulkan。
现在更改包名称来匹配公司和项目(你之前设置的),并将最低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”作为其着色器。
注意:如果跳过此步骤,那么在远程用户视频流启动时,在你的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 以远程用户身份进行测试。
现在你已经有了一些现场视频,返回Unity编辑器并选择File> Build Settings。将修改后的房间场景拖放到“要构建的场景”一栏。注意,新的Oculus具有其构建脚本,因此你不仅要在创建Unity Editor的窗口上单击“构建并运行”,而是要转到Oculus菜单,然后选择“生成并运行”。屏幕截图如下。
短暂的加载序列后,你的app就会部署到Oculus Quest头显设备中。恭喜你!如果你想在Oculus Quest头显设备中运行app而且不需要重建,请在头显设备内部进入“Library”>“ UnkownSources”以查看你 的app。做得好!
其他资源
- 适用于Unity的可在Unity资产商店中找到的 Agora.io Video SDK。
- 在此处获取Web app的源代码 。
- 完整的API文档可在“ 文档中心中找到]
- 要获得技术支持,请使用Agora Dashboard 提交工单 。
- 完整的Oculus文档可 在此处获得。
作者 Rick Cheng
原文链接 https://www.agora.io/en/blog/how-to-embed-drag-drop-video-streaming-vr-oculus-quest/