NVIDIA CloudXR Client SDK 文件¶
{sdk-root-folder}\Client
文件夹包含构成 NVIDIA CloudXR SDK 的头文件和库文件。您必须在应用程序代码中包含头文件,并在编译应用程序时链接到相应的库文件。
文件 |
包含路径的文件名 |
---|---|
头文件 |
{sdk-root-folder}\Client\Include\CloudXRClient.h {sdk-root-folder}\Client\Include\CloudXRCommon.h {sdk-root-folder}\Client\Include\CloudXRInputEvents.h |
Windows 操作系统库文件 |
{sdk-root-folder}\Client\Lib\Windows\CloudXRClient.lib |
Android 操作系统库文件 |
{sdk-root-folder}\Client\Lib\Android\CloudXR.aar |
iOS 操作系统库文件 |
{sdk-root-folder}\Client\Lib\iOS\libCloudXR.a |
iOS StreamSDK 框架 |
{sdk-root-folder}\Client\Lib\iOS\StreamSdk.framework |
要在大多数平台上创建新应用程序,请复制或引用应用程序项目或 makefile 中的库/头文件。
对于 Android,将 CloudXR.AAR 文件复制到您的项目目录中,通常是 {project-root}\app\libs
目录,并且 build.gradle
脚本可以解压缩此文件。此解压缩过程会生成所需的头文件和库,因此您无需复制或引用基本头文件。有关更多信息,请查看现有的 CloudXR Android 示例。
示例客户端应用程序源代码¶
{sdk-root-folder}\Sample
文件夹包含源代码和相关的第三方代码,用于构建 Windows、Android 和 iOS 操作系统上受支持客户端硬件的示例应用程序。示例应用程序包括
开发 CloudXR 客户端¶
概述¶
在较高层面上,CloudXR 客户端应用程序中存在以下阶段
一个简单的 main()
函数的伪代码可能如下所示
int main() { MySetupDeviceDesc(&ddesc); MySetupCallbacks(&callbacks); MySetupReceiverDesc(&rdesc, ddesc, callbacks)cxrCreateReceiver()
;cxrConnect()
; while (!*exiting*) { MyPlatformEventHandling(); if (client_state < connected) { MyRenderConnectionProgress(); } else if (client_state == connected) { UpdateTrackingState();cxrLatchFrame()
MyRenderFrames(framesLatched); // NOTE: on Android you must callcxrBlitFrame()
cxrReleaseFrame()
// NOTE: every n frames you may callcxrGetConnectionStats()
} else { MyHandleDisconnect(); exiting = true; } }cxrDestroyReceiver()
} Note that UpdateTrackingState is a placeholder in this pseudo-code for the tracking state callback, which happens in another thread if asynchronous, or could happen inline here if synchronous. Inside of that function is where you now handle controller addition and binding of inputs, removal, and collecting and sending input events.
客户端设置¶
要设置客户端
分配设备描述符结构(参见
cxrDeviceDesc
)。此分配包含有关硬件设备属性或其他特定运行时设置的详细信息,包括视频流计数以及每个流的详细信息,包括格式、分辨率、FPS 和比特率(参见cxrClientVideoStreamDesc
)、音频支持、轮询输入的频率以及启用/禁用某些 CloudXR 服务器功能(如姿势预测、虚拟 VSync 和注视点渲染)的标志。注意
许多客户端结构和设置已更改,包括新的流式传输配置、新的控制器设置/处理、一些已删除的选项等。您将需要查看结构文档和示例应用程序代码,以便更好地了解自定义客户端迁移到 CloudXR 4.0 需要哪些更新。
例如,设备描述符不再具有 cxrDeliveryType,而是现在在每个流的基础上设置宽度、高度和格式。因此,将 AR 流式传输到平板电脑设备将设置为 1 个 RGBA 流,而将 VR 流式传输到 HMD 将设置为 2 个 RGB 流,并将 isStereo 设置为 true。
以下是 Windows 示例客户端的一部分,它具有描述符的成员结构,并填充了如下信息字段
float fps = 0.0f; uint32_t width = 0; uint32_t height = 0; int32_t x, y; d->GetWindowBounds(&x, &y, &width, &height); fps = m_hmd->GetFloatTrackedDeviceProperty(k_unTrackedDeviceIndex_Hmd, Prop_DisplayFrequency_Float); m_deviceDesc.numVideoStreamDescs = CXR_NUM_VIDEO_STREAMS_XR; for (uint32_t i = 0; i < m_deviceDesc.numVideoStreamDescs; i++) { m_deviceDesc.videoStreamDescs[i].format = cxrClientSurfaceFormat_RGB; m_deviceDesc.videoStreamDescs[i].width = width / 2; m_deviceDesc.videoStreamDescs[i].height = height; m_deviceDesc.videoStreamDescs[i].fps = std::min(CXR_MAX_VIDEO_STREAM_FPS, fps); m_deviceDesc.videoStreamDescs[i].maxBitrate = options.mMaxVideoBitrate; } m_deviceDesc.stereoDisplay = (2==m_deviceDesc.numVideoStreamDescs); m_deviceDesc.maxResFactor = options.mMaxResFactor; m_deviceDesc.ipd = m_hmd->GetFloatTrackedDeviceProperty(k_unTrackedDeviceIndex_Hmd, Prop_UserIpdMeters_Float); m_deviceDesc.predOffset = 0; for (int i = 0; i < 2; i++) { m_hmd->GetProjectionRaw((vr::EVREye)i, &m_deviceDesc.proj[i][0], &m_deviceDesc.proj[i][1], &m_deviceDesc.proj[i][2], &m_deviceDesc.proj[i][3]); } m_deviceDesc.receiveAudio = m_clientOptions.mReceiveAudio; m_deviceDesc.sendAudio = m_clientOptions.mSendAudio; m_deviceDesc.embedInfoInVideo = false; m_deviceDesc.foveatedScaleFactor = m_clientOptions.mFoveation; m_deviceDesc.foveationModeCaps = 0; m_deviceDesc.posePollFreq = 0; m_deviceDesc.disablePosePrediction = false; m_deviceDesc.angularVelocityInDeviceSpace = false; GetChaperone(&m_deviceDesc.chaperone);
设置客户端回调结构(参见
cxrClientCallbacks
),其中包含指向您的应用程序支持的回调函数的指针。如果应用程序希望服务器与最新的视图和输入更改同步,则必须实现
GetTrackingState
回调。如果客户端应用程序想要支持播放来自服务器的音频,请实现
RenderAudio
回调,并将音频缓冲区传递给某些音频播放系统 - 请参阅示例,了解跨平台的几种方法。所有客户端通常都希望实现
UpdateClientState
回调,以接收连接状态更改的通知,这对于异步连接尤为重要,并接收流式传输期间预期和意外断开连接的通知。CloudXR 运行时不再“拥有”消息日志记录,这是 4.0 版本的新功能。相反,如果您想捕获来自 CloudXR 运行时的日志消息,则客户端应用程序需要实现
LogMessage
回调,并将该信息传递给某些客户端日志记录系统。如果您还没有日志解决方案,您可以查看 CloudXRFileLogger,它包含在 SDK 的 shared 目录中,并被所有示例客户端使用,以近似 CloudXR 过去所做的内部日志文件输出 - 例外是 iOS 客户端,它在 Swift 中实现了日志记录类的基本实现。在非 iOS 示例中,您将看到正在使用的日志记录宏,它们都路由到一个静态“dispatch”函数。在客户端内部,如果您使用这些日志记录宏,它们将直接重定向到您自己的 dispatch 函数。在 CloudXR 库内部,相同的宏会转到库内部的 dispatch 函数,如果您注册了 LogMessage 回调,它会将消息传递给客户端应用程序进行处理。如果客户端没有消息回调,则 CloudXR 内部回退只是将日志记录到每个平台的相应标准/调试输出。您不必在客户端中使用此方法,但它是一种方便的模式。
此处的目的(与许多其他最近的更改一样)是为客户端应用程序提供更多的控制权。如果您已经使用了一些第三方日志记录库,那么您可以简单地根据需要格式化字段,并将其交给记录器,CloudXR 消息将立即直接集成到您的应用程序日志中。
准备接收器描述符结构(参见
cxrReceiverDesc
)。此步骤复制设备描述符和客户端回调,为所有回调设置客户端上下文,这可能是单例对象指针或其他全局结构,设置流式传输要求,并设置各种调试/日志记录选项,包括应用程序特定的输出数据路径。有关在不同平台上设置输出路径的更具体详细信息,请参阅 CloudXR 文件存储。
对于 Windows 客户端,填写接收器描述符可能如下所示
cxrReceiverDesc desc = {}; cxrClientCallbacks callbacks = {}; // fill out callbacks with your supported callbacks, including the new LogMessage callback, and the important UpdateClientState callback. // See samples for how each app sets up these callback. callbacks.GetTrackingState = ... callbacks.TriggerHaptic = ... callbacks.RenderAudio = ... callbacks.ReceiveUserData = ... callbacks.UpdateClientState = ... callbacks.LogMessage = ... // Note the callbacks struct now holds the *context* for the client instead of the receiver descriptor. callbacks.clientContext = this; // Start filling in receiver descriptor fields. // This is where you set your app-specific output path strncpy(desc.appOutputPath, outputPath.c_str(), CXR_MAX_PATH - 1); desc.appOutputPath[CXR_MAX_PATH - 1] = 0; desc.requestedVersion = CLOUDXR_VERSION_DWORD; desc.deviceDesc = deviceDesc; desc.clientCallbacks = clientCallbacks; desc.shareContext = nullptr; desc.debugFlags = m_clientOptions.mDebugFlags; desc.logMaxSizeKB = m_clientOptions.mLogMaxSizeKB;
注意
对于不支持端到端 sRGB 的设备,您需要标记它,以便 CloudXR 将切换到线性模式。就像这样简单
m_receiverDesc.debugFlags |= cxrDebugFlags_OutputLinearRGBColor;
准备好所有结构和字段后,客户端现在可以调用
cxrCreateReceiver()
,传入接收器描述符,以及指向cxrReceiverHandle
的指针,以保存返回的接收器句柄,该句柄是与 CloudXR SDK 进行所有进一步交互所必需的。如果失败,请向用户报告错误(并记录它),并干净地退出。如果接收器的创建成功,请使用
cxrConnect()
启动与服务器的连接。同样,从 Windows 客户端示例来看,这可能看起来像这样m_connectionDesc.async = true; m_connectionDesc.useL4S = m_clientOptions.mUseL4S; m_connectionDesc.clientNetwork = m_clientOptions.mClientNetwork; m_connectionDesc.topology = m_clientOptions.mTopology; cxrError err = cxrConnect(m_receiver, m_clientOptions.mServerIP.c_str(), &m_connectionDesc);
参数是接收器对象、服务器 IP 和连接描述符。
IP 假定为点分数字 IPv4 地址,这可能是手动输入的,使用 DNS 或自定义匹配之类的东西转换的,或者从 mDNS/Bonjour 之类的东西收集的。
连接描述符
cxrConnectionDesc
字段有助于建立连接,并告知服务器客户端知道的有关连接的信息。一般来说,传递来自启动选项的值以进行配置是一个好方法,除非是针对非常特定的设备/配置的定制应用程序。connectionDesc.async = cxrTrue; connectionDesc.maxVideoBitrateKbps = launch_options_.mMaxVideoBitrate; connectionDesc.clientNetwork = launch_options_.mClientNetwork; connectionDesc.topology = launch_options_.mTopology; connectionDesc.useL4S = launch_options_.mUseL4S
一个重要的字段是
async
,如果为 true,则指示库使用后台线程启动与服务器的连接,如果为 false,则立即在当前线程上运行。如果您决定使用异步模式(推荐),请确保您在UpdateClientState
回调中实现连接状态更改的处理。如果连接失败,请通知用户有关错误,然后退出或返回到您的连接 UI,以允许用户重试。
如果连接成功,请在应用程序中设置一个状态以指示流式传输已准备就绪,并在主循环中,处理流式传输状态更改并开始渲染帧。
客户端主循环¶
客户端状态¶
应用程序的主循环可能需要处理不同的状态,并确定在每种状态下要执行和渲染的操作。
注意
许多状态直接映射到 cxrClientState
中的值。
接收器创建之前
如果主循环不仅包含 CloudXR 流式传输,并且在达到某些状态之前不实例化 CloudXR 客户端,则应用程序可能会渲染 UI 以与用户交互,或者只是显示加载指示器。
连接之前
应用程序可能会显示加载指示器,或者在异步连接到服务器的情况下,它可能会显示正在连接到服务器指示器。
成功连接后
当使用异步标志调用
cxrConnect()
或在同步模式下但在后台线程中调用时,主循环需要使用标志或状态变量来识别到成功流式传输的过渡。然后,应用程序可以开始淡入过渡或显示连接已建立消息。渲染流式传输帧
在应用程序建立与服务器的连接后,它就可以开始从网络接收视频帧。每次通过连接时的主循环,代码都需要确定是否有可用的帧。在此之后,检索最新的帧,渲染输出,然后释放回系统。此过程将在以下部分中介绍。
断开连接后
检测到断开连接状态后,应用程序可能需要设置标志或进行调用,以指示其他系统流式传输已完成,并且需要清理和关闭与 CloudXR 会话相关的任何内容。如果发生意外断开连接,应用程序可能会显示错误消息并退出或返回到初始连接界面。
CloudXR 输入系统¶
使用 CloudXR 4.0,客户端上向服务器发送输入事件的系统已从头开始重写,其设计目标是行业标准。从代码参考中,请参阅 cxrAddController()
、cxrFireControllerEvents()
和 cxrRemoveController()
。
注意
所有设备/客户端必须使用新的输入系统,旧的代码和结构在 CloudXR 4.0 版本中不再存在。
我们当然已将 SteamVR 驱动程序更新到新系统,并使用顶级应用程序对其进行了彻底测试,以确保 SteamVR 映射/配置文件已正确连接,并且与 CloudXR 3.x 相比,具有复杂配置文件的应用程序(如 Half-Life: Alyx)完全可以正常运行。
此外,新的实验性服务器示例是围绕新系统设计的,它具有一组它可以绑定的操作、它支持的所有可能输入的主列表以及管理客户端输入到服务器操作映射的配置文件系统。如果这一切听起来很熟悉,那应该是的,因为如上所述,我们在设计时考虑了未来的客户端和服务器。虽然它是与 Quest 客户端修订版协同开发的,但它应该适用于所有客户端 - 尽管它可能在功能方面受到限制,具体取决于客户端。
- 在新系统中,您首先通过
cxrAddController()
向服务器注册“新的”控制器(您尚未“看到”且尚未注册的控制器)。您传入一个cxrControllerDesc
,用于描述服务器的控制器 数字标识符,目前对于左控制器必须为 0,对于右控制器必须为 1。对于其他输入设备,可以是任何值。
一个定义其“角色”的字符串。对于默认控制器,我们选择了自定义 URI “cxr://input/hand/left” 和 “cxr://input/hand/right”,因为我们的输入路径当前不需要路径中的手部命名。对于其他输入设备,它可以是任何标识其用途的东西。
控制器的产品名称,用于识别可视模型和配置文件绑定。
输入的计数、输入路径表和每个输入的数据类型表,总体上定义了控制器可以并且将产生哪些输入。
提供的 SteamVR 服务器驱动程序和实验性示例服务器支持的输入路径主列表是
static const char* inputPathsGeneric[] =
{
"/input/system/click",
"/input/application_menu/click",
"/input/trigger/click",
"/input/trigger/touch",
"/input/trigger/value",
"/input/trackpad/click", // valve and htc have trackpads on PC HMDs.
"/input/trackpad/touch",
"/input/trackpad/x",
"/input/trackpad/y",
"/input/joystick/click", // oculus steam driver historically uses 'joystick' term
"/input/joystick/touch",
"/input/joystick/x",
"/input/joystick/y",
"/input/x/click",
"/input/y/click",
"/input/a/click",
"/input/b/click",
"/input/x/touch",
"/input/y/touch",
"/input/a/touch",
"/input/b/touch",
"/input/thumb_rest/touch",
"/input/grip/click",
"/input/grip/touch",
"/input/grip/value",
"/input/grip/force",
"/input/thumbstick/click", // valve steam driver historically uses 'thumbstick' term
"/input/thumbstick/touch",
"/input/thumbstick/x",
"/input/thumbstick/y",
};
注意
有关代码中新系统的完整示例,请查看 Oculus 客户端示例,因为它展示了实现对新控制器/输入系统支持的合理方法。它在首次检测到控制器处于活动状态时动态注册控制器,提供相应的输入路径数组。并且在轮询控制器的输入状态时,它会生成一个 cxrControllerEvent
数组,该数组采用输入路径索引和不同格式数据的联合(布尔值、整数、浮点数),并在完成后调用 cxrFireControllerEvents()
以将输入事件列表发送到服务器。
渲染流式视频¶
帧获取¶
要确定是否有可用的视频帧,请调用 cxrLatchFrame()
以尝试按顺序获取下一个帧。第一个感兴趣的参数是 cxrFramesLatched
结构,它需要具有一个作用域,以便它将存在到渲染完成为止。返回后,它会填充有关已获取帧的信息。
下一个参数是用于从中获取帧的帧/流的位掩码。大多数应用程序只需传递 cxrFrameMask_All
即可告诉系统以锁步方式从所有流中获取帧。这通常是 XR/AR/VR 的情况,但在大多数情况下也适用于通用模式连接。
但是,某些通用模式应用程序可能一次需要一个流,因此它们将按索引循环遍历视频流总数,并且可以一次锁定一个流,每次传递 1<<index
作为掩码。如果特定索引是众所周知的,他们还可以获得特定的“子集”流,只需将流位掩码 OR 在一起即可。例如,要抓取流 0 和 3,您需要传递 1<<0 | 1<<3
。
LatchFrame
的最后一个参数是以毫秒为单位的超时值。如果帧未准备好,该参数将短暂休眠,再次检查,并重复此过程,直到超时时间已过。一般来说,超时时间将是帧长度的一个因子,合理的起始值是半显示刷新率(或 2000/displayHz
)。此值允许在帧不可用时调用返回。这样,如果帧传递延迟,应用程序可以将周期提供给主循环中的其他系统。短超时值还提供了渲染某些缓存视觉效果到屏幕和/或指示流式传输延迟的机会。如果应用程序希望手动管理休眠,则超时值为零将导致检查和快速返回,而无需任何休眠。
帧渲染¶
如果锁定失败,应用程序可以跳过渲染或渲染某些缓存内容,然后继续通过主循环。这使其他系统有机会在帧延迟的情况下运行/更新,并确保任何逐帧状态检查逻辑(例如处理输入或客户端状态更改(如断开连接))在合理的时间段内发生。
如果锁定成功,则返回的 cxrFramesLatched
结构保存了渲染帧所需的帧数据。对于 Android,有一个 API 调用 cxrBlitFrame()
应该在设置渲染目标和视口之后调用,它将使用共享的 OpenGL|ES 上下文来正确 blit 输出锁定的帧(包括处理 AR 流的 alpha 混合或 VR 流的去注视点渲染之类的事情)。对于其他平台,去注视点渲染等后处理部分在解码步骤中处理,然后由应用程序知道解码帧数据采用什么数据格式以及如何根据给定的图形 API 适当地渲染(blit/提交)。
帧释放¶
渲染完成后,必须调用 cxrReleaseFrame()
,以告知 CloudXR 您已使用锁定的帧,并且 CloudXR 可以在内部释放和回收。
更新头戴式显示器属性¶
要与姿势跟踪更新一起更新头戴式显示器投影参数、刷新率或 IPD,必须在跟踪结构中设置 HasProjection
、HasRefresh
或 HasIPD
标志,并在 cxrHmdTrackingState
的 proj
、displayRefresh
或 ipd
字段中设置新的投影参数、刷新率或 IPD 值。
注意
某些服务器和/或客户端可能无法正确响应刷新率或 IPD 的实时更改,因为它们不是为动态调整这些值而设计的。
连接统计信息¶
可以定期调用 cxrGetConnectionStats()
以监视连接的运行状况。我们建议在调用之间等待 fps * 3
帧被锁定(约 3 秒)。如何在示例客户端中执行此操作以及如何解释统计信息的示例。
客户端清理¶
在退出应用程序之前,释放连接到 CloudXR 的资源。至少,您必须调用 cxrDestroyReceiver()
,这将刷新内部缓冲区和共享句柄。