Replaces manual H264/TCP stereo streaming with NVIDIA CloudXR for
higher-quality stereoscopic rendering and lower latency.
Changes:
- teleop_xr_agent.py: add --cloudxr flag (enables Isaac Sim XR mode,
disables manual StreamingManager)
- deps/cloudxr/: NVIDIA CloudXR.js SDK (Early Access) with Isaac Lab
teleop React web client
- deps/cloudxr/Dockerfile.wss.proxy: HAProxy WSS proxy for PICO 4 Ultra
HTTPS mode (routes wss://48322 → ws://49100)
- deps/cloudxr/isaac/webpack.dev.js: disable file watching to avoid
EMFILE errors with large node_modules
- deps/cloudxr/INSTALL.md: full setup guide
Usage:
# Start CloudXR Runtime + Isaac Lab
cd ~/IsaacLab && ./docker/container.py start \
--files docker-compose.cloudxr-runtime.patch.yaml \
--env-file .env.cloudxr-runtime
# Run teleop with CloudXR
~/IsaacLab/isaaclab.sh -p teleop_xr_agent.py \
--task Isaac-MindRobot-2i-DualArm-IK-Abs-v0 --cloudxr
# Serve web client
cd deps/cloudxr/isaac && npm run dev-server:https
593 lines
19 KiB
HTML
593 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
|
|
<meta name="robots" content="noindex, nofollow">
|
|
<meta name="description" content="NVIDIA CloudXR.js Sample Client for VR/AR streaming">
|
|
<title>NVIDIA CloudXR.js Sample Client</title>
|
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
|
<style>
|
|
:root {
|
|
--primary-green: #76b900;
|
|
--border-color: #e0e0e0;
|
|
--text-main: #111;
|
|
--background-main: #fff;
|
|
--input-radius: 0px;
|
|
--input-border: 2px solid var(--border-color);
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
|
|
html,
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
background: var(--background-main);
|
|
color: var(--text-main);
|
|
touch-action: manipulation;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
@supports (padding: max(0px)) {
|
|
body {
|
|
padding: max(0px, env(safe-area-inset-top)) max(0px, env(safe-area-inset-right)) max(0px, env(safe-area-inset-bottom)) max(0px, env(safe-area-inset-left));
|
|
}
|
|
}
|
|
|
|
/* Top Banner */
|
|
.top-banner {
|
|
width: 100vw;
|
|
height: 8px;
|
|
background: var(--primary-green);
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
/* Header */
|
|
header {
|
|
background: var(--background-main);
|
|
padding: 16px 0 16px 24px;
|
|
min-height: unset;
|
|
box-shadow: none;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
h1 {
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
main {
|
|
display: flex;
|
|
height: calc(100vh - 64px);
|
|
}
|
|
|
|
aside {
|
|
width: 540px;
|
|
min-width: 400px;
|
|
padding: 32px 24px 24px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
border-right: 1px solid var(--border-color);
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.settings-title {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
/* Inputs and Selectors */
|
|
.ui-input,
|
|
select {
|
|
width: 100%;
|
|
min-width: 300px;
|
|
padding: 16px 12px;
|
|
font-size: 1rem;
|
|
border: var(--input-border);
|
|
border-radius: var(--input-radius);
|
|
background: var(--background-main);
|
|
min-height: 48px;
|
|
}
|
|
|
|
.ui-input {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.ui-input:focus,
|
|
select:focus {
|
|
outline: none;
|
|
border-color: var(--primary-green);
|
|
box-shadow: 0 0 0 3px rgba(118, 185, 0, 0.2);
|
|
}
|
|
|
|
.ui-input::placeholder {
|
|
color: #999;
|
|
}
|
|
|
|
.settings-table {
|
|
width: 100%;
|
|
margin-bottom: 24px;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.settings-table td {
|
|
padding: 12px 0;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.settings-table label {
|
|
font-weight: 600;
|
|
display: inline-block;
|
|
min-width: 140px;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.settings-table select {
|
|
width: 200px;
|
|
padding: 12px 8px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
button {
|
|
background: var(--primary-green);
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: var(--input-radius);
|
|
padding: 16px 24px;
|
|
font-size: 1.1rem;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
min-height: 56px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
button:hover {
|
|
background: #8ac800;
|
|
}
|
|
|
|
button:disabled {
|
|
background: #666;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.start-button {
|
|
margin-top: 16px;
|
|
width: auto;
|
|
font-size: 1rem;
|
|
padding: 12px 20px;
|
|
color: #000;
|
|
}
|
|
|
|
section {
|
|
flex: 1;
|
|
padding: 48px 32px 32px;
|
|
background: #f8f8f8;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.debug-title {
|
|
font-weight: 700;
|
|
margin-bottom: 16px;
|
|
font-size: 1.3rem;
|
|
}
|
|
|
|
.debug-list {
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
color: #666;
|
|
}
|
|
|
|
.debug-list li {
|
|
padding: 8px 0;
|
|
border-bottom: 1px solid var(--border-color);
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.debug-list li:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
main {
|
|
flex-direction: column;
|
|
height: auto;
|
|
min-height: calc(100vh - 64px);
|
|
}
|
|
|
|
html,
|
|
body {
|
|
overflow: auto;
|
|
}
|
|
|
|
aside {
|
|
width: 100%;
|
|
min-width: auto;
|
|
border-right: none;
|
|
border-bottom: 1px solid #404040;
|
|
}
|
|
|
|
section {
|
|
padding: 24px 16px;
|
|
}
|
|
|
|
header {
|
|
font-size: 1.1rem;
|
|
padding: 12px 16px;
|
|
}
|
|
|
|
.settings-table select {
|
|
width: 100%;
|
|
max-width: 200px;
|
|
}
|
|
}
|
|
|
|
@media (prefers-contrast: high) {
|
|
|
|
body,
|
|
aside,
|
|
.ui-input,
|
|
select {
|
|
background: var(--background-main);
|
|
color: #000;
|
|
}
|
|
|
|
aside {
|
|
border-color: #000;
|
|
}
|
|
|
|
section {
|
|
background: #f0f0f0;
|
|
}
|
|
|
|
.ui-input,
|
|
select {
|
|
border-color: #000;
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
* {
|
|
animation-duration: 0.01ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: 0.01ms !important;
|
|
}
|
|
}
|
|
|
|
.ui-input:focus-visible,
|
|
select:focus-visible,
|
|
button:focus-visible {
|
|
outline: 3px solid var(--primary-green);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.input-label {
|
|
display: block;
|
|
font-size: 0.92rem;
|
|
color: #444;
|
|
margin-bottom: 5px;
|
|
margin-top: 10px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.input-label:first-child {
|
|
margin-top: 0;
|
|
}
|
|
|
|
/* Console Logs Section */
|
|
.console-logs-section {
|
|
margin-top: 24px;
|
|
}
|
|
|
|
.console-logs-textarea {
|
|
height: 300px;
|
|
font-family: 'Courier New', monospace;
|
|
resize: vertical;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.console-logs-buttons {
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.console-logs-button {
|
|
background: #666;
|
|
font-size: 0.9rem;
|
|
padding: 8px 16px;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.console-logs-button:last-child {
|
|
margin-right: 0;
|
|
}
|
|
|
|
/* Configuration Section */
|
|
.config-section {
|
|
margin-top: 24px;
|
|
}
|
|
|
|
.config-input {
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.config-text {
|
|
font-size: 0.85rem;
|
|
color: #666;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
/* Exit Button */
|
|
.exit-button {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: #ff3300cc;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
padding: 10px 15px;
|
|
font-weight: bold;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
display: none;
|
|
z-index: 10000;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.exit-button:hover {
|
|
background: #ff3300;
|
|
transform: scale(1.05);
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
/* Status Message Box */
|
|
.status-message-box {
|
|
display: none;
|
|
background: #ffebee;
|
|
border: 2px solid #f44336;
|
|
border-radius: 4px;
|
|
padding: 12px 16px;
|
|
margin: 16px 0;
|
|
color: #c62828;
|
|
font-size: 0.9rem;
|
|
font-weight: 500;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.status-message-box.show {
|
|
display: block;
|
|
}
|
|
|
|
/* Success state - green */
|
|
.status-message-box.success {
|
|
background: #e8f5e9;
|
|
border-color: var(--primary-green);
|
|
color: #2e7d32;
|
|
}
|
|
|
|
/* Info state - light blue/green */
|
|
.status-message-box.info {
|
|
background: #e3f2fd;
|
|
border-color: #2196f3;
|
|
color: #1565c0;
|
|
}
|
|
|
|
/* Error state - red (default) */
|
|
.status-message-box.error {
|
|
background: #ffebee;
|
|
border-color: #f44336;
|
|
color: #c62828;
|
|
}
|
|
|
|
.status-message-box .status-icon {
|
|
display: inline-block;
|
|
margin-right: 8px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* Certificate Acceptance Link */
|
|
.cert-acceptance-link {
|
|
background: #e3f2fd;
|
|
border: 2px solid #2196f3;
|
|
border-radius: 4px;
|
|
padding: 12px 16px;
|
|
margin: 8px 0 16px 0;
|
|
font-size: 0.9rem;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.cert-acceptance-link .cert-icon {
|
|
display: inline-block;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.cert-acceptance-link a {
|
|
color: #1565c0;
|
|
text-decoration: none;
|
|
font-weight: 600;
|
|
border-bottom: 1px solid #1565c0;
|
|
}
|
|
|
|
.cert-acceptance-link a:hover {
|
|
color: #0d47a1;
|
|
border-bottom-color: #0d47a1;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="top-banner"></div>
|
|
<header>
|
|
<h1>NVIDIA CloudXR.js Sample Client</h1>
|
|
</header>
|
|
|
|
<main>
|
|
<aside>
|
|
<button id="startButton" class="start-button" type="button" aria-label="Connect" disabled>CONNECT</button>
|
|
|
|
<div id="statusMessageBox" class="status-message-box" role="alert" aria-live="polite">
|
|
<span class="status-icon">⚠</span>
|
|
<span id="statusMessageText"></span>
|
|
</div>
|
|
|
|
<h2 class="settings-title">Settings</h2>
|
|
|
|
<form id="settingsForm">
|
|
<label for="serverType" class="input-label">Select Server Backend</label>
|
|
<select id="serverType" class="ui-input">
|
|
<option value="manual">Manual Input IP:Port</option>
|
|
<option value="nvcf" disabled>NVCF</option>
|
|
</select>
|
|
|
|
<div id="manualFields">
|
|
<label for="serverIpInput" class="input-label">Server IP</label>
|
|
<input id="serverIpInput" class="ui-input" type="text" placeholder="Server IP" spellcheck="false">
|
|
<label for="portInput" class="input-label">Port</label>
|
|
<input id="portInput" class="ui-input" type="number" placeholder="Port (default: 49100)"
|
|
spellcheck="false" min="1" max="65535">
|
|
<div id="certAcceptanceLink" class="cert-acceptance-link" style="display: none;">
|
|
<span class="cert-icon">🔒</span>
|
|
<a id="certLink" href="#" target="_blank" rel="noopener noreferrer"></a>
|
|
</div>
|
|
</div>
|
|
|
|
<label for="app" class="input-label">Application</label>
|
|
<select id="app" name="app" class="ui-input" aria-label="Select application">
|
|
<option value="generic">Generic Client</option>
|
|
<!-- TODO: Add other applications here -->
|
|
</select>
|
|
<label for="immersive" class="input-label">Immersive Mode</label>
|
|
<select id="immersive" name="immersive" class="ui-input" aria-label="Select immersive mode">
|
|
<option value="ar" selected>AR Immersive</option>
|
|
<option value="vr">VR Immersive</option>
|
|
</select>
|
|
</form>
|
|
</aside>
|
|
|
|
<section>
|
|
<h3 class="debug-title">Debug Settings</h3>
|
|
|
|
<div class="config-section">
|
|
<label class="input-label">Proxy Configuration</label>
|
|
<label for="proxyUrl" class="input-label">Proxy URL</label>
|
|
<input id="proxyUrl" class="ui-input config-input" type="text" placeholder="" spellcheck="false" autocapitalize="off">
|
|
<div class="config-text">
|
|
<span id="proxyDefaultText"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<label class="input-label">Frame Rate Configuration</label>
|
|
<label for="deviceFrameRate" class="input-label">Device Frame Rate</label>
|
|
<select id="deviceFrameRate" class="ui-input config-input">
|
|
<option value="72">72 FPS</option>
|
|
<option value="90" selected>90 FPS</option>
|
|
<option value="120">120 FPS</option>
|
|
</select>
|
|
<div class="config-text">
|
|
Select the target device frame rate for the XR session
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<label class="input-label">Maximum Streaming Bitrate</label>
|
|
<label for="maxStreamingBitrateMbps" class="input-label">Megabits per second</label>
|
|
<select id="maxStreamingBitrateMbps" class="ui-input config-input">
|
|
<option value="80">80 Mbps</option>
|
|
<option value="100">100 Mbps</option>
|
|
<option value="120">120 Mbps</option>
|
|
<option value="150" selected>150 Mbps</option>
|
|
<option value="180">180 Mbps</option>
|
|
<option value="200">200 Mbps</option>
|
|
</select>
|
|
<div class="config-text">
|
|
Select the maximum streaming bitrate (in Megabits per second) for the XR session
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<label class="input-label">Stream Resolution Configuration</label>
|
|
<label for="perEyeWidth" class="input-label">Per-Eye Width (default: 2048)</label>
|
|
<input id="perEyeWidth" class="ui-input config-input" type="number" placeholder="Per-eye width in pixels"
|
|
spellcheck="false" value="2048">
|
|
<label for="perEyeHeight" class="input-label">Per-Eye Height (default: 1792)</label>
|
|
<input id="perEyeHeight" class="ui-input config-input" type="number" placeholder="Per-eye height in pixels"
|
|
spellcheck="false" value="1792">
|
|
<div class="config-text">
|
|
Configure the per-eye resolution. Width and height must be multiples of 16.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<label for="referenceSpace" class="input-label">Preferred Reference Space</label>
|
|
<select id="referenceSpace" class="ui-input config-input">
|
|
<option value="auto" selected>Auto (local-floor preferred)</option>
|
|
<option value="local-floor">local-floor</option>
|
|
<option value="local">local</option>
|
|
<option value="unbounded">unbounded</option>
|
|
<option value="viewer">viewer</option>
|
|
</select>
|
|
<div class="config-text">
|
|
Select the preferred reference space for XR tracking. "Auto" uses the original fallback logic (local-floor → local → viewer). Other options will attempt to use the specified space only.
|
|
</div>
|
|
<label class="input-label">XR Reference Space Offset</label>
|
|
<label for="xrOffsetX" class="input-label">X Offset (centimeters): Horizontal</label>
|
|
<input id="xrOffsetX" class="ui-input config-input" type="number" placeholder="X offset in centimeters"
|
|
spellcheck="false" value="0" step="1" min="-1000" max="1000">
|
|
<label for="xrOffsetY" class="input-label">Y Offset (centimeters): Vertical</label>
|
|
<input id="xrOffsetY" class="ui-input config-input" type="number" placeholder="Y offset in centimeters"
|
|
spellcheck="false" value="0" step="1" min="-1000" max="1000">
|
|
<label for="xrOffsetZ" class="input-label">Z Offset (centimeters): Depth (facing outward)</label>
|
|
<input id="xrOffsetZ" class="ui-input config-input" type="number" placeholder="Z offset in centimeters"
|
|
spellcheck="false" value="0" step="1" min="-1000" max="1000">
|
|
<div class="config-text">
|
|
Configure the XR reference space offset in centimeters. These values will be applied to the reference space transform to adjust the origin position of the XR experience.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<label for="enablePoseSmoothing" class="input-label">Pose Smoothing</label>
|
|
<select id="enablePoseSmoothing" class="ui-input config-input">
|
|
<option value="true" selected>Enabled</option>
|
|
<option value="false">Disabled</option>
|
|
</select>
|
|
<div class="config-text">
|
|
Enable or disable secondary smoothing on predicted positions to reduce jitter. This only affects position, not orientation.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<label for="posePredictionFactor" class="input-label">Pose Prediction Factor: <span id="posePredictionFactorValue">1.0</span></label>
|
|
<input id="posePredictionFactor" class="ui-input config-input" type="range"
|
|
min="0" max="1" step="0.1" value="1.0"
|
|
style="padding: 8px 12px;">
|
|
<div class="config-text">
|
|
Scale the pose prediction horizon (0.0 = no prediction, 1.0 = full prediction). This multiplier affects both position and orientation prediction strength.
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<!-- WebGL Canvas for CloudXR rendering -->
|
|
<canvas id="webglCanvas"></canvas>
|
|
|
|
<!-- Floating Exit Button. This is a fallback option when immersive mode has no way to exit. -->
|
|
<button id="exitButton" class="exit-button" type="button" aria-label="Exit">Exit</button>
|
|
</body>
|
|
|
|
</html> |