Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,24 @@
}
_icons = _VERSION_ICON_MAP.get(_smv_name, _DEFAULT_ICONS)

# Branch-specific CloudXR web client ("CloudXR.js") deployment. docs.yaml
# publishes the prebuilt web client to ``/client/<slug>/`` where ``<slug>`` is
# the built ref name with ``/`` replaced by ``-`` (e.g. ``main``,
# ``release-1.3.x``, ``v1.2.3``). Resolving the same slug here lets each
# versioned docs build link to the matching client instead of always ``main``.
_client_slug = (_smv_name or "main").replace("/", "-")
_web_client_url = f"https://nvidia.github.io/IsaacTeleop/client/{_client_slug}/"

# Shared substitution + link targets injected into every page, so the
# branch-specific web client URL lives in one place. ``|web_client_url|``
# expands the bare URL (usable in prose and ``parsed-literal`` blocks); the
# named targets back ```...`_`` references in the prose.
rst_epilog = f"""
.. |web_client_url| replace:: {_web_client_url}
.. _`nvidia.github.io/IsaacTeleop/client`: {_web_client_url}
.. _`Isaac Teleop Web Client`: {_web_client_url}
"""

html_theme_options = {
"collapse_navigation": True,
"use_edit_page_button": True,
Expand Down
2 changes: 1 addition & 1 deletion docs/source/getting_started/build_from_source/webxr.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Build WebXR Client

.. tip::

A prebuilt CloudXR web client is available at https://nvidia.github.io/IsaacTeleop/client/main/.
A prebuilt CloudXR web client is available at |web_client_url|.
You only need to build from source if you want to customize the client.

The Isaac Teleop WebXR client is built with React, Three.js, and WebXR. It streams VR/AR content
Expand Down
1 change: 0 additions & 1 deletion docs/source/getting_started/quick_start.rst
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,6 @@ More Information

..
References
.. _`nvidia.github.io/IsaacTeleop/client`: https://nvidia.github.io/IsaacTeleop/client/main/
.. _`CloudXR documentation`: https://docs.nvidia.com/cloudxr-sdk/latest/index.html
.. _`Isaac XR Teleop Sample Client for Apple Vision Pro`: https://github.com/isaac-sim/isaac-xr-teleop-sample-client-apple
.. _`Isaac Lab Quick Start`: https://isaac-sim.github.io/IsaacLab/develop/source/setup/quickstart.html
Expand Down
1 change: 0 additions & 1 deletion docs/source/overview/ecosystem.rst
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,6 @@ directly. Each device name in the tables above links to the corresponding manufa

.. Other references
.. _`Isaac XR Teleop Sample Client`: https://github.com/isaac-sim/isaac-xr-teleop-sample-client-apple
.. _`Isaac Teleop Web Client`: https://nvidia.github.io/IsaacTeleop/client
.. _`Manus Gloves Plugin`: https://github.com/NVIDIA/IsaacTeleop/tree/main/src/plugins/manus
.. _`Generic 3-axis Pedal Plugin`: https://github.com/NVIDIA/IsaacTeleop/tree/main/src/plugins/generic_3axis_pedal
.. _`OAK-D Camera Plugin`: https://github.com/NVIDIA/IsaacTeleop/tree/main/src/plugins/oak
8 changes: 4 additions & 4 deletions docs/source/references/oob_teleop_control.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ If the adb automation fails (e.g. headset not paired), you can manually open
the client URL on the headset browser with **all three** required query
parameters — ``oobEnable``, ``serverIP``, and ``port``:

.. code-block:: text
.. parsed-literal::

https://nvidia.github.io/IsaacTeleop/client/main/?oobEnable=1&serverIP=<HOST_IP>&port=48322
|web_client_url|\ ?oobEnable=1&serverIP=<HOST_IP>&port=48322
Comment thread
yanziz-nvidia marked this conversation as resolved.

Replace ``<HOST_IP>`` with the streaming host's LAN IP. The ``port`` must
match the proxy port (default 48322).
Expand Down Expand Up @@ -310,9 +310,9 @@ Web client integration
The WebXR client connects to the hub when the page URL contains
``oobEnable=1`` plus ``serverIP`` and ``port``:

.. code-block:: text
.. parsed-literal::

https://nvidia.github.io/IsaacTeleop/client/main/?oobEnable=1&serverIP=10.0.0.1&port=48322
|web_client_url|\ ?oobEnable=1&serverIP=10.0.0.1&port=48322

The client builds ``wss://{serverIP}:{port}/oob/v1/ws`` and:

Expand Down
23 changes: 8 additions & 15 deletions src/core/cloudxr/python/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import argparse
import os
import re
import signal
import sys
import time
Expand Down Expand Up @@ -34,6 +33,7 @@
usb_backend_port,
usb_turn_port,
usb_ui_port,
versioned_web_client_url,
)


Expand Down Expand Up @@ -84,7 +84,8 @@ def _parse_args() -> argparse.Namespace:
f"and HTTPS static WebXR UI on port {usb_ui_port()} "
"(override via USB_UI_PORT env). Files live under "
"TELEOP_WEB_CLIENT_STATIC_DIR or ~/.cloudxr/static-client; missing "
"index.html / bundle.js are downloaded from nvidia.github.io/IsaacTeleop/client/main/. "
"index.html / bundle.js are downloaded from the matching versioned "
"client under nvidia.github.io/IsaacTeleop/client/. "
"The launcher serves them with the same PEM as the WSS proxy. "
"Requirements: `coturn`, `adb` on PATH. WebRTC ICE still needs a "
"non-loopback interface on the headset (WiFi stays connected)."
Expand Down Expand Up @@ -182,20 +183,12 @@ def main() -> None:
)
print_oob_hub_startup_banner(lan_host=resolve_lan_host_for_oob())
else:
# Print the WebXR client matching this release line, derived from
# the installed version's leading MAJOR.MINOR (present for every
# build type). The client is published per release line on GitHub
# Pages. In OOB modes the banner above already prints a complete,
# mode-correct client URL instead (GitHub Pages /main/ for WiFi, a
# local https URL for USB-local), so this standalone line is
# Print the WebXR client matching the installed version. In OOB
# modes the banner above already prints a complete, mode-correct
# client URL instead (the same versioned GitHub Pages client for
# WiFi, a local https URL for USB-local), so this standalone line is
# skipped there to avoid a redundant or misleading second URL.
client_base = "https://nvidia.github.io/IsaacTeleop/client/"
ver_match = re.match(r"(\d+)\.(\d+)", isaacteleop_version)
client_url = (
f"{client_base}release-{ver_match.group(1)}.{ver_match.group(2)}.x/"
if ver_match
else client_base
)
client_url = versioned_web_client_url(isaacteleop_version)
print(f"WebXR client: \033[36m{client_url}\033[0m")
print(
f"Activate CloudXR environment in another terminal: \033[1;32msource {env_cfg.env_filepath()}\033[0m"
Expand Down
10 changes: 5 additions & 5 deletions src/core/cloudxr/python/oob_teleop_adb.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
import time
import urllib.request
from .oob_teleop_env import (
DEFAULT_WEB_CLIENT_ORIGIN,
default_web_client_origin,
parse_env_port,
build_headset_bookmark_url,
client_ui_fields_from_env,
Expand Down Expand Up @@ -486,7 +486,7 @@ def build_teleop_url(*, resolved_port: int, usb_local: bool = False) -> str:
**client_ui_fields_from_env(),
}
ovr = web_client_base_override_from_env()
web_base = ovr if ovr else DEFAULT_WEB_CLIENT_ORIGIN
web_base = ovr if ovr else default_web_client_origin()

token = os.environ.get("CONTROL_TOKEN") or None
return build_headset_bookmark_url(
Expand Down Expand Up @@ -1199,7 +1199,7 @@ def clear_headset_browser_cache(*, usb_local: bool) -> int:
Returns 0 if the browser isn't running. Never raises.
"""
from .oob_teleop_env import ( # noqa: PLC0415
DEFAULT_WEB_CLIENT_ORIGIN,
default_web_client_origin,
usb_ui_port,
web_client_base_override_from_env,
)
Expand Down Expand Up @@ -1240,9 +1240,9 @@ def clear_headset_browser_cache(*, usb_local: bool) -> int:
origins.append(f"https://127.0.0.1:{ui_port}")
else:
origin_base = (
web_client_base_override_from_env() or DEFAULT_WEB_CLIENT_ORIGIN
web_client_base_override_from_env() or default_web_client_origin()
)
# DEFAULT_WEB_CLIENT_ORIGIN has trailing slash + path; reduce to origin.
# The client base has a trailing slash + path; reduce to origin.
from urllib.parse import urlparse # noqa: PLC0415

parsed = urlparse(origin_base)
Expand Down
55 changes: 48 additions & 7 deletions src/core/cloudxr/python/oob_teleop_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import sys
import threading
import time
from importlib.metadata import PackageNotFoundError, version
from pathlib import Path
from urllib.error import URLError
from urllib.parse import urlencode, urljoin
Expand All @@ -26,11 +27,50 @@

WSS_PROXY_DEFAULT_PORT = 48322

DEFAULT_WEB_CLIENT_ORIGIN = "https://nvidia.github.io/IsaacTeleop/client/main/"
# GitHub Pages WebXR client root. The published client lives under a per-ref
# slug (``main``, ``release-1.3.x``, ``v1.2.3``, ...); the docs build emits one
# slug per ref it builds. :func:`default_web_client_origin` resolves the slug
# for the installed version so OOB opens the matching client.
WEB_CLIENT_BASE = "https://nvidia.github.io/IsaacTeleop/client/"

# Origin used when the installed version can't be resolved (dev trees, tests).
FALLBACK_WEB_CLIENT_ORIGIN = urljoin(WEB_CLIENT_BASE, "main/")


def versioned_web_client_url(version: str) -> str:
"""GitHub Pages WebXR client URL matching *version*.

A clean ``MAJOR.MINOR.PATCH`` release (a tag build) maps to the per-tag
client ``client/vMAJOR.MINOR.PATCH/``. Pre-release / dev builds (``1.2.4rc1``,
``1.3.0.dev5``, ...) and any other version with a leading MAJOR.MINOR map to
the release line ``client/release-MAJOR.MINOR.x/``. Versions with no
parseable MAJOR.MINOR fall back to the generic ``client/`` URL, which the
site redirects to the latest stable tag. The same helper backs the
standalone "WebXR client:" line printed in non-OOB mode, so every path
agrees on which client to open.
"""
v = version.strip()
if re.fullmatch(r"\d+\.\d+\.\d+", v):
return urljoin(WEB_CLIENT_BASE, f"v{v}/")
m = re.match(r"(\d+)\.(\d+)", v)
if m:
return urljoin(WEB_CLIENT_BASE, f"release-{m.group(1)}.{m.group(2)}.x/")
return WEB_CLIENT_BASE


def default_web_client_origin() -> str:
"""Versioned WebXR client origin for the installed ``isaacteleop`` version.

Reads the installed distribution version (namespace-independent, so it works
under the test package alias too) and maps it via
:func:`versioned_web_client_url`. Falls back to
:data:`FALLBACK_WEB_CLIENT_ORIGIN` when the version can't be determined.
"""
try:
return versioned_web_client_url(version("isaacteleop"))
except PackageNotFoundError:
return FALLBACK_WEB_CLIENT_ORIGIN

# Published WebXR client files (same origin as ``DEFAULT_WEB_CLIENT_ORIGIN``).
USB_LOCAL_STATIC_INDEX_URL = urljoin(DEFAULT_WEB_CLIENT_ORIGIN, "index.html")
USB_LOCAL_STATIC_BUNDLE_URL = urljoin(DEFAULT_WEB_CLIENT_ORIGIN, "bundle.js")

# Upper bound for downloaded client assets (supply-chain / accident guard).
_USB_LOCAL_ASSET_MAX_BYTES = 32 * 1024 * 1024
Expand Down Expand Up @@ -154,9 +194,10 @@ def require_usb_local_webxr_static_dir() -> Path:
f"Cannot create USB-local WebXR static directory {p}: {exc}"
) from exc

client_origin = default_web_client_origin()
assets = (
("index.html", USB_LOCAL_STATIC_INDEX_URL),
("bundle.js", USB_LOCAL_STATIC_BUNDLE_URL),
("index.html", urljoin(client_origin, "index.html")),
("bundle.js", urljoin(client_origin, "bundle.js")),
)
for name, url in assets:
dest = p / name
Expand Down Expand Up @@ -504,7 +545,7 @@ def print_oob_hub_startup_banner(
or f"https://localhost:{ui_port}"
)
else:
web_base = DEFAULT_WEB_CLIENT_ORIGIN
web_base = default_web_client_origin()

stream_cfg: dict = {"serverIP": primary_host, "port": port}
if usb_local:
Expand Down
54 changes: 54 additions & 0 deletions src/core/cloudxr_tests/python/test_oob_teleop_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,20 @@
import pytest

from cloudxr_py_test_ns.oob_teleop_env import (
FALLBACK_WEB_CLIENT_ORIGIN,
TELEOP_WEB_CLIENT_BASE_ENV,
TELEOP_WEB_CLIENT_STATIC_DIR_ENV,
USB_BACKEND_DEFAULT_PORT,
USB_TURN_DEFAULT_PORT,
USB_UI_DEFAULT_PORT,
WEB_CLIENT_BASE,
WSS_PROXY_DEFAULT_PORT,
build_headset_bookmark_url,
client_ui_fields_from_env,
default_initial_stream_config,
default_web_client_origin,
guess_lan_ipv4,
versioned_web_client_url,
print_oob_hub_startup_banner,
require_usb_local_webxr_static_dir,
resolve_lan_host_for_oob,
Expand Down Expand Up @@ -130,6 +134,56 @@ def test_web_client_base_override_from_env(
assert web_client_base_override_from_env() == "https://example.test/app"


def test_versioned_web_client_url_exact_tag() -> None:
# A clean MAJOR.MINOR.PATCH release maps to the per-tag client.
assert (
versioned_web_client_url("1.2.3")
== "https://nvidia.github.io/IsaacTeleop/client/v1.2.3/"
)


def test_versioned_web_client_url_prerelease_uses_release_line() -> None:
# rc / dev builds fall to the release line, not a per-tag client.
assert (
versioned_web_client_url("1.3.9rc1")
== "https://nvidia.github.io/IsaacTeleop/client/release-1.3.x/"
)
assert (
versioned_web_client_url("1.3.0.dev5")
== "https://nvidia.github.io/IsaacTeleop/client/release-1.3.x/"
)


def test_versioned_web_client_url_unparseable_falls_back_to_base() -> None:
# No leading MAJOR.MINOR -> generic client root (site redirects to stable).
assert versioned_web_client_url("unknown") == WEB_CLIENT_BASE


def test_default_web_client_origin_uses_installed_version(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import cloudxr_py_test_ns.oob_teleop_env as mod

monkeypatch.setattr(mod, "version", lambda _name: "1.4.0", raising=False)
assert (
default_web_client_origin()
== "https://nvidia.github.io/IsaacTeleop/client/v1.4.0/"
)


def test_default_web_client_origin_falls_back_when_version_missing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import cloudxr_py_test_ns.oob_teleop_env as mod
from importlib.metadata import PackageNotFoundError

def _raise(_name: str) -> str:
raise PackageNotFoundError(_name)

monkeypatch.setattr(mod, "version", _raise, raising=False)
assert default_web_client_origin() == FALLBACK_WEB_CLIENT_ORIGIN


def test_build_headset_bookmark_url_minimal() -> None:
u = build_headset_bookmark_url(
web_client_base="https://h.test/",
Expand Down
Loading