使用 HolographicSpace API 编写全息远程处理远程应用
如果你是全息远程处理新手,建议阅读概述。
重要
本文档介绍如何使用 HolographicSpace API 创建适用于 HoloLens 2 远程应用程序。 适用于 “HoloLens(第一代)”的远程应用程序必须使用 NuGet 包版本 “1.xx”。这意味着,为 HoloLens 2 编写的远程应用程序与 HoloLens 1 不兼容,反之亦然。 可在此处找到关于 HoloLens 1 的文档。
弃用通知:2.9.x 版本线将是支持使用 Windows 全息 API 进行应用程序开发的最后一个版本线。 即将推出的版本仅支持使用 OpenXR 进行应用程序开发。 除此之外,我们建议在你的应用程序中使用 OpenXR 进行所有新的应用程序开发。 使用 2.9 或更早版本的现有应用程序将继续正常运行,不受即将进行的更改的影响。
全息远程处理应用可将远程呈现的内容流式传输到 HoloLens 2 和 Windows Mixed Reality 沉浸式头戴显示设备。 你还可以访问更多系统资源,并将远程沉浸式视图集成到现有的台式电脑软件中。 远程应用从 HoloLens 2 接收输入数据流,在虚拟沉浸式视图中呈现内容,并将内容帧流式传输回 HoloLens 2。 使用标准 Wi-Fi 建立连接。 全息远程处理通过 NuGet 包添加到桌面或 UWP 应用。 需要使用额外的代码来处理连接,并在沉浸式视图中进行呈现。 典型的远程连接会有低至 50 毫秒的延迟。 播放器应用可以实时报告延迟。
本页上的所有代码和工作项目可以在全息远程处理示例 github 存储库中找到。
先决条件
一个很好的起点是面向 Windows Mixed Reality API 的基于 DirectX 的桌面或 UWP 应用。 有关详细信息,请参阅 DirectX 开发概述。 C++ 全息项目模板是一个很好的起点。
重要
应将任何使用全息远程处理的应用编写成使用多线程单元。 支持使用单线程单元,但这会导致性能欠佳,并可能在播放过程中出现卡顿。 使用 C++/WinRT winrt::init_apartment 时,多线程单元是默认设置。
获取全息远程处理 NuGet 包
若要在 Visual Studio 中将 NuGet 包添加到某个项目,需要执行以下步骤。
- 在 Visual Studio 中打开项目。
- 右键单击项目节点,然后选择“管理 NuGet 包...”
- 在出现的面板中选择“浏览”,然后搜索“全息远程处理”。
- 选择“Microsoft.Holographic.Remoting”,确保选择最新的 “2.x.x” 版本,然后选择“安装”。
- 如果出现了“预览”对话框,请选择“确定”。
- 弹出许可协议对话框时,请选择“我接受”。
注意
“1.xx” 版本的 NuGet 包仍然可供要将 HoloLens 1 作为目标的开发人员使用。 有关详细信息,请参阅添加 Holographic Remoting(HoloLens(第一代))。
创建远程上下文
首先,应用程序应创建一个远程上下文。
// class declaration
#include <winrt/Microsoft.Holographic.AppRemoting.h>
...
private:
// RemoteContext used to connect with a Holographic Remoting player and display rendered frames
winrt::Microsoft::Holographic::AppRemoting::RemoteContext m_remoteContext = nullptr;
// class implementation
#include <HolographicAppRemoting\Streamer.h>
...
CreateRemoteContext(m_remoteContext, 20000, false, PreferredVideoCodec::Default);
警告
Holographic Remoting 的工作原理是将用作 Windows 一部分的 Windows Mixed Reality 运行时替换为远程处理特定的运行时。 此操作是在创建远程上下文期间完成的。 因此,在创建远程上下文之前对任何 Windows Mixed Reality API 进行任何调用都可能导致意外行为。 建议的方法是在与任何混合现实 API 交互之前尽早创建远程上下文。 切勿将在调用 CreateRemoteContext 之前通过任何 Windows Mixed Reality API 创建或检索的对象与之后创建或检索的对象混合使用。
接下来需要创建全息空间。 不需要指定 CoreWindow。 没有 CoreWindow 的桌面应用只能传递 nullptr
。
m_holographicSpace = winrt::Windows::Graphics::Holographic::HolographicSpace::CreateForCoreWindow(nullptr);
连接到设备
当远程应用准备好呈现内容时,可与播放器设备建立连接。
可通过以下两种方式之一进行连接。
- 远程应用连接到设备上运行的播放器。
- 在设备上运行的播放器连接到远程应用。
若要建立从远程应用到播放器设备的连接,请在指定主机名和端口的远程上下文中调用 Connect
方法。 Holographic Remoting 播放器使用的端口是 8265。
try
{
m_remoteContext.Connect(m_hostname, m_port);
}
catch(winrt::hresult_error& e)
{
DebugLog(L"Connect failed with hr = 0x%08X", e.code());
}
重要
与任何 C++/WinRT API 一样,Connect
可能会引发需要处理的 winrt::hresult_error。
提示
若要避免使用 C++/WinRT 语言投影,可以包含位于 Holographic Remoting NuGet 包中的文件 build\native\include\<windows sdk version>\abi\Microsoft.Holographic.AppRemoting.h
。 该文件包含底层 COM 接口的声明。 不过,建议使用 C++/WinRT。
可以通过调用 Listen
方法来侦听远程应用上的传入连接。 在进行这种调用期间,可以指定握手端口和传输端口。 握手端口用于初始握手。 然后通过传输端口发送数据。 默认使用 8265 和 8266。
try
{
m_remoteContext.Listen(L"0.0.0.0", m_port, m_port + 1);
}
catch(winrt::hresult_error& e)
{
DebugLog(L"Listen failed with hr = 0x%08X", e.code());
}
重要
NuGet 包中的 build\native\include\HolographicAppRemoting\Microsoft.Holographic.AppRemoting.idl
包含 Holographic Remoting 公开的 API 的详细文档。
处理远程处理特定事件
远程上下文公开三个事件,这些事件对于监视连接状态非常重要。
- OnConnected:与设备成功建立连接时触发。
winrt::weak_ref<winrt::Microsoft::Holographic::AppRemoting::IRemoteContext> remoteContextWeakRef = m_remoteContext;
m_onConnectedEventRevoker = m_remoteContext.OnConnected(winrt::auto_revoke, [this, remoteContextWeakRef]() {
if (auto remoteContext = remoteContextWeakRef.get())
{
// Update UI state
}
});
- OnDisconnected:建立的连接关闭或无法建立连接时触发。
m_onDisconnectedEventRevoker =
m_remoteContext.OnDisconnected(winrt::auto_revoke, [this, remoteContextWeakRef](ConnectionFailureReason failureReason) {
if (auto remoteContext = remoteContextWeakRef.get())
{
DebugLog(L"Disconnected with reason %d", failureReason);
// Update UI
// Reconnect if this is a transient failure.
if (failureReason == ConnectionFailureReason::HandshakeUnreachable ||
failureReason == ConnectionFailureReason::TransportUnreachable ||
failureReason == ConnectionFailureReason::ConnectionLost)
{
DebugLog(L"Reconnecting...");
ConnectOrListen();
}
// Failure reason None indicates a normal disconnect.
else if (failureReason != ConnectionFailureReason::None)
{
DebugLog(L"Disconnected with unrecoverable error, not attempting to reconnect.");
}
}
});
- OnListening:开始侦听传入连接时触发。
m_onListeningEventRevoker = m_remoteContext.OnListening(winrt::auto_revoke, [this, remoteContextWeakRef]() {
if (auto remoteContext = remoteContextWeakRef.get())
{
// Update UI state
}
});
此外,可以使用远程上下文中的 ConnectionState
属性查询连接状态。
auto connectionState = m_remoteContext.ConnectionState();
处理语音事件
借助远程语音接口,可以使用 HoloLens 2 注册语音触发器并将其远程处理到远程应用程序。
需要提供以下附加成员才能跟踪远程语音的状态:
winrt::Microsoft::Holographic::AppRemoting::IRemoteSpeech::OnRecognizedSpeech_revoker m_onRecognizedSpeechRevoker;
首先检索远程语音接口。
if (auto remoteSpeech = m_remoteContext.GetRemoteSpeech())
{
InitializeSpeechAsync(remoteSpeech, m_onRecognizedSpeechRevoker, weak_from_this());
}
然后可以使用异步帮助器方法初始化远程语音。 应以异步方式完成此操作,因为初始化可能需要相当长的时间。 使用 C++/WinRT 执行并发和异步操作介绍了如何使用 C++/WinRT 编写异步函数。
winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::Storage::StorageFile> LoadGrammarFileAsync()
{
const wchar_t* speechGrammarFile = L"SpeechGrammar.xml";
auto rootFolder = winrt::Windows::ApplicationModel::Package::Current().InstalledLocation();
return rootFolder.GetFileAsync(speechGrammarFile);
}
winrt::fire_and_forget InitializeSpeechAsync(
winrt::Microsoft::Holographic::AppRemoting::IRemoteSpeech remoteSpeech,
winrt::Microsoft::Holographic::AppRemoting::IRemoteSpeech::OnRecognizedSpeech_revoker& onRecognizedSpeechRevoker,
std::weak_ptr<SampleRemoteMain> sampleRemoteMainWeak)
{
onRecognizedSpeechRevoker = remoteSpeech.OnRecognizedSpeech(
winrt::auto_revoke, [sampleRemoteMainWeak](const winrt::Microsoft::Holographic::AppRemoting::RecognizedSpeech& recognizedSpeech) {
if (auto sampleRemoteMain = sampleRemoteMainWeak.lock())
{
sampleRemoteMain->OnRecognizedSpeech(recognizedSpeech.RecognizedText);
}
});
auto grammarFile = co_await LoadGrammarFileAsync();
std::vector<winrt::hstring> dictionary;
dictionary.push_back(L"Red");
dictionary.push_back(L"Blue");
dictionary.push_back(L"Green");
dictionary.push_back(L"Default");
dictionary.push_back(L"Aquamarine");
remoteSpeech.ApplyParameters(L"", grammarFile, dictionary);
}
可通过两种方式指定要识别的短语。
- 语音语法 xml 文件中的规范。 有关详细信息,请参阅如何创建基本的 XML 语法。
- 通过在字典向量中将其传递给
ApplyParameters
来指定。
在 OnRecognizedSpeech 回调中可以处理语音事件:
void SampleRemoteMain::OnRecognizedSpeech(const winrt::hstring& recognizedText)
{
bool changedColor = false;
DirectX::XMFLOAT4 color = {1, 1, 1, 1};
if (recognizedText == L"Red")
{
color = {1, 0, 0, 1};
changedColor = true;
}
else if (recognizedText == L"Blue")
{
color = {0, 0, 1, 1};
changedColor = true;
}
else if (recognizedText == L"Green")
{
...
}
...
}
在本地预览流式传输内容
若要在远程应用中显示发送到设备的相同内容,可以使用远程上下文的 OnSendFrame
事件。 每当 Holographic Remoting 库将当前帧发送到远程设备时,都会触发 OnSendFrame
事件。 这是提取内容并将其位块传送 (blit) 到桌面或 UWP 窗口的理想时间。
#include <windows.graphics.directx.direct3d11.interop.h>
...
m_onSendFrameEventRevoker = m_remoteContext.OnSendFrame(
winrt::auto_revoke, [this](const winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DSurface& texture) {
winrt::com_ptr<ID3D11Texture2D> texturePtr;
{
winrt::com_ptr<ID3D11Resource> resource;
winrt::com_ptr<::IInspectable> inspectable = texture.as<::IInspectable>();
winrt::com_ptr<Windows::Graphics::DirectX::Direct3D11::IDirect3DDxgiInterfaceAccess> dxgiInterfaceAccess;
winrt::check_hresult(inspectable->QueryInterface(__uuidof(dxgiInterfaceAccess), dxgiInterfaceAccess.put_void()));
winrt::check_hresult(dxgiInterfaceAccess->GetInterface(__uuidof(resource), resource.put_void()));
resource.as(texturePtr);
}
// Copy / blit texturePtr into the back buffer here.
});
深度重新投影
从版本 2.1.0 开始,Holographic Remoting 支持深度重新投影。 这需要将颜色缓冲区和深度缓冲区从远程应用程序流式传输到 HoloLens 2。 默认情况下,深度缓冲区流式传输已启用,并配置为使用颜色缓冲区的一半分辨率。 可按如下所示更改此配置:
// class implementation
#include <HolographicAppRemoting\Streamer.h>
...
CreateRemoteContext(m_remoteContext, 20000, false, PreferredVideoCodec::Default);
// Configure for half-resolution depth.
m_remoteContext.ConfigureDepthVideoStream(DepthBufferStreamResolution::Half_Resolution);
请注意,如果不应使用默认值,则必须在与 HoloLens 2 建立连接之前调用 ConfigureDepthVideoStream
。 最佳时机是在创建远程上下文之后。 DepthBufferStreamResolution 的可能值为:
- Full_Resolution
- Half_Resolution
- Quarter_Resolution
- Disabled(已在版本 2.1.3 中添加,如果使用此值,则不会创建额外的深度视频流)
请记住,使用全分辨率深度缓冲区也会影响带宽要求,需要考虑到提供给 CreateRemoteContext
的最大带宽值。
除了配置分辨率之外,还必须通过 HolographicCameraRenderingParameters.CommitDirect3D11DepthBuffer 提交深度缓冲区。
void SampleRemoteMain::Render(HolographicFrame holographicFrame)
{
...
m_deviceResources->UseHolographicCameraResources([this, holographicFrame](auto& cameraResourceMap) {
...
for (auto cameraPose : prediction.CameraPoses())
{
DXHelper::CameraResources* pCameraResources = cameraResourceMap[cameraPose.HolographicCamera().Id()].get();
...
m_deviceResources->UseD3DDeviceContext([&](ID3D11DeviceContext3* context) {
...
// Commit depth buffer if available and enabled.
if (m_canCommitDirect3D11DepthBuffer && m_commitDirect3D11DepthBuffer)
{
auto interopSurface = pCameraResources->GetDepthStencilTextureInteropObject();
HolographicCameraRenderingParameters renderingParameters = holographicFrame.GetRenderingParameters(cameraPose);
renderingParameters.CommitDirect3D11DepthBuffer(interopSurface);
}
});
}
});
}
若要验证深度重新投影是否在 HoloLens 2 上正常工作,可以通过设备门户启用深度可视化工具。 有关详细信息,请参阅验证是否正确设置了深度。
可选:自定义数据通道
使用自定义数据通道可以通过已建立的远程连接发送用户数据。 有关详细信息,请参阅自定义数据通道。
可选:坐标系统同步
从版本 2.7.0 开始,坐标系同步可用于对齐播放器与远程应用程序之间的空间数据。 有关详细信息,请参阅 Holographic Remoting 概述中的“坐标系同步”。