Files
mindbot/deps/cloudxr/simple/index.html
yt lee 623e05f250 Add CloudXR VR streaming support for PICO 4 Ultra (Early Access)
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
2026-03-26 14:29:03 +08:00

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>