Files
mindbot/deps/cloudxr/react/webpack-plugins/DownloadAssetsPlugin.js
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

110 lines
2.8 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const https = require('https');
class DownloadAssetsPlugin {
constructor(assets) {
this.assets = assets;
this.hasRun = false;
this.createdDirs = new Set();
}
safeUnlink(filePath) {
try {
fs.unlinkSync(filePath);
} catch (err) {
// Ignore cleanup errors
}
}
apply(compiler) {
compiler.hooks.beforeCompile.tapAsync('DownloadAssetsPlugin', (params, callback) => {
// Only run once per webpack process
if (this.hasRun) {
callback();
return;
}
console.log('📦 Checking and downloading required assets...');
const downloadPromises = this.assets.map(asset => this.downloadFile(asset));
Promise.allSettled(downloadPromises)
.then((results) => {
const failed = results.filter(r => r.status === 'rejected');
if (failed.length > 0) {
console.warn(`⚠️ ${failed.length} asset(s) failed to download, continuing anyway...`);
}
console.log('✅ Asset check complete!');
this.hasRun = true;
callback();
});
});
}
downloadFile({ url, output }) {
return new Promise((resolve, reject) => {
// Ensure directory exists (only once per unique path)
if (!this.createdDirs.has(output)) {
fs.mkdirSync(output, { recursive: true });
this.createdDirs.add(output);
}
const filename = path.basename(url);
const filePath = path.join(output, filename);
// Skip if file already exists
if (fs.existsSync(filePath)) {
resolve();
return;
}
console.log(` Downloading ${filename}...`);
const file = fs.createWriteStream(filePath);
const downloadFromUrl = (downloadUrl) => {
https.get(downloadUrl, (response) => {
// Handle redirects
if (response.statusCode === 302 || response.statusCode === 301) {
file.close();
this.safeUnlink(filePath);
downloadFromUrl(response.headers.location);
return;
}
if (response.statusCode !== 200) {
file.close();
this.safeUnlink(filePath);
reject(new Error(`Failed to download ${filename}: HTTP ${response.statusCode}`));
return;
}
response.pipe(file);
file.on('finish', () => {
file.close();
console.log(` ✓ Downloaded ${filename}`);
resolve();
});
file.on('error', (err) => {
this.safeUnlink(filePath);
reject(err);
});
}).on('error', (err) => {
if (fs.existsSync(filePath)) {
this.safeUnlink(filePath);
}
reject(err);
});
};
downloadFromUrl(url);
});
}
}
module.exports = DownloadAssetsPlugin;