Skip to content

About

The zenoh-web command: zenoh-web with the ROS 2 / dimos codecs

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

zenoh-gateway-cli

The zenoh-gateway command: a zenoh-gateway server with the ROS 2 / dimos codecs of zenoh-dimos-codecs. View and drive a zenoh system from a browser over WebRTC: camera images as H.264 video (hardware-encoded where it can), lossless depth, quantized point clouds, raw bytes for everything else, and a per-browser bandwidth allocator. The client API, the allocator and the wire protocol are documented in zenoh-gateway's README and SPEC.md.

the example page: topic list, H.264 video, point cloud, depth, a raw stream and live allocation stats

Install

Prebuilt binaries (Linux x86_64/aarch64 with glibc ≥ 2.35, macOS Apple Silicon/Intel; no Windows):

curl -fsSL https://raw.githubusercontent.com/dimensionalOS/zenoh-gateway-cli/main/install.sh | sh

It picks the release tarball for your OS/CPU, checks it against SHA256SUMS, and installs zenoh-gateway into ~/.local/bin. Env overrides: ZENOH_GATEWAY_VERSION=v0.3.0, ZENOH_GATEWAY_INSTALL_DIR=/somewhere/bin.

With nix (builds from source; aarch64-darwin, aarch64-linux, x86_64-linux):

nix profile install github:dimensionalOS/zenoh-gateway-cli   # puts zenoh-gateway on your PATH
nix run github:dimensionalOS/zenoh-gateway-cli -- --help     # or run it without installing

Quick start

git clone https://github.com/dimensionalOS/zenoh-gateway-cli && cd zenoh-gateway-cli
cargo run --release -- --serve examples/web --connect tcp/127.0.0.1:7447   # your zenoh router/peer's endpoint
# open http://localhost:7448/

No robot handy? Start the test peer first; it publishes zenoh-dimos-codecs' fixtures (a 320×240 image, a 20000-point cloud and a 16-bit depth image, in the dimos message format) while someone subscribes (FIXTURES = that repository's test/fixtures):

cargo run --release --example test_peer -- --listen tcp/127.0.0.1:7447 \
    --publish demo/camera/sensor_msgs.Image=$FIXTURES/dimos/image_rgb8.bin@10 \
    --publish demo/lidar/sensor_msgs.PointCloud2=$FIXTURES/dimos/pointcloud_xyzi.bin@10 \
    --publish demo/depth/sensor_msgs.Image=$FIXTURES/dimos/depth_16UC1.bin@10

Then on the page type a key (e.g. demo/camera/sensor_msgs.Image; the encoding select guesses dimos_lcm_image; the channel select defaults to the encoding's own) and press Add. A recent stable Rust (edition 2024), or nix develop for a shell with Rust, deno, zig and cargo-zigbuild.

The example page (examples/web/, plain JS) imports zenoh-gateway's client from /client/zenoh_gateway.js, the bundle deno task build makes (serve its output: zenoh-gateway --serve build, then open /examples/index.html), so nothing comes from the internet. Depth and point clouds arrive as zenoh-gateway fields that the client decodes itself (msg.decoded), zstd-compressed by default; the encodings and what each sends come from the server (client.encodings). Query parameters: ?gateway=<url> (default: the page's origin) and ?client=<module url> (another build of the client).

Flags

flag default
--port <n> 7448 HTTP (HTTPS with --cert) port: POST /offer signaling and static files
--zenoh-config <file> zenoh defaults (peer, multicast scouting) zenoh json5 config, including access_control
--connect <endpoint> zenoh endpoint, e.g. tcp/192.168.1.2:7447; repeatable
--serve <dir> serve a directory (/ → index.html), live-editable on disk
--cert <pem>, --key <pem> none (plain HTTP) serve HTTPS with this certificate chain and private key, so tokens never cross the network in the clear
--max-bandwidth-bytes-per-sec <n> none cap every browser's budget below its estimate (known-slow link, tests)
--auth-file <file> none (anyone may do anything) require a token: json5 map of tokens to grants, see Auth
--slowest-lease-heartbeat-allowed <secs> 60 refuse a lease to a browser whose heartbeat deadline (heartbeatMisses / heartbeatHz) is longer: a frozen page keeps its leases that long
--ice-server <url> none STUN/TURN for both ends, e.g. stun:stun.l.google.com:19302, turn:user:pass@relay.example:3478; repeatable
--turn-secret <secret> none coturn's static-auth-secret: TURN servers without user:pass@ get credentials minted per connection
--turn-ttl <seconds> 86400 how long --turn-secret credentials last
--ice-servers-command <cmd> none mint STUN/TURN servers per connection, added after --ice-server: run with sh -c, env ZENOH_GATEWAY_ICE_SIDE (browser/gateway) and ZENOH_GATEWAY_ICE_TOKEN; prints {"iceServers": [...]} or [...]. On failure or after 5 s, only --ice-server
--cloudflare-turn-key-id <id> none Cloudflare TURN: credentials minted through Cloudflare's API; the key's API token comes from CLOUDFLARE_TURN_API_TOKEN (an env var, so it stays out of ps)
--cloudflare-turn-ttl <seconds> 86400 how long Cloudflare credentials last; the shared set is minted again after half of it
--cloudflare-turn-per-connection off mint Cloudflare credentials for every connection instead of sharing one set
--udp-ports <port or low-high> ephemeral WebRTC UDP port range, one port per browser connection (firewall-friendly)
--video-encoder <name> auto auto (the first hardware encoder that encodes a test frame, else software), software (openh264), videotoolbox (macOS), gstreamer (nvv4l2h264enc on a Jetson, nvh264enc, vah264enc / vaapih264enc; GStreamer is loaded at runtime, so the binary runs without it); from zenoh-web-encoders. A hardware encoder that fails mid-stream hands over to software
--zenoh-signalling <name> none also answer signalling over zenoh as <name> (queryables zenoh-gateway/<name>/offer, /ice), for a zenoh-gateway-relay this gateway's zenoh dials out to; see Behind a relay
--no-http off no HTTP listener at all (needs --zenoh-signalling): nothing listens for inbound connections

Logging: RUST_LOG=info,zenoh=warn. Access control is zenoh's own: an access_control section in --zenoh-config applies to the browsers' traffic like to any other (denied puts and subscriptions are dropped silently), e.g. so no browser can drive the robot:

{
    mode: "peer",
    connect: { endpoints: ["tcp/192.168.1.2:7447"] },
    access_control: {
        enabled: true,
        default_permission: "allow",
        rules: [
            { id: "no-browser-cmd-vel", messages: ["put"], flows: ["egress", "ingress"], permission: "deny", key_exprs: ["**/cmd_vel/**"] },
        ],
        subjects: [{ id: "anyone" }],
        policies: [{ rules: ["no-browser-cmd-vel"], subjects: ["anyone"] }],
    },
}

Auth

--auth-file tokens.json5 makes every browser present a token (connect(url, { token }), sent as Authorization: Bearer). The file maps tokens to a role or a full zenoh-gateway Grant, and may define lease groups:

{
    tokens: {
        "viewer-7f3a": "read",    // subscribe, get and listTopics on everything
        "ops-91bc": "write",      // read + publish everything
        "driver-c04d": "lease",   // write + lease any group
        "safety-55e1": { subscribe: ["**"], publish: ["robot/**"], leaseGroups: ["*"], forceExpire: true, maxLeaseSecs: 600 },
        "arm-cam": { subscribe: ["robot/arm/camera/**"], listTopics: ["robot/arm/**"] },
    },
    leaseGroups: { drive: ["robot/cmd_vel/**"], arm: ["robot/arm/cmd/**"] },
}

The file is re-read when it changes: removing (or changing) a token revokes it, closing its live connections and refusing its reconnects. Lease groups are read at startup. Leases bind only the browsers of this gateway, not native zenoh publishers (zenoh's access_control covers those). Grants, leases and ICE: zenoh-gateway's README and SPEC ("Auth", "Leases", "ICE and TURN").

A relay with coturn: turnserver --use-auth-secret --static-auth-secret=$SECRET --realm=robots, then zenoh-gateway --ice-server turn:relay.example.org:3478 --turn-secret $SECRET --udp-ports 50000-50100; the gateway and every browser use the relay with credentials minted per connection.

Cloudflare TURN instead: CLOUDFLARE_TURN_API_TOKEN=$TOKEN zenoh-gateway --cloudflare-turn-key-id $KEY_ID (Cloudflare dashboard → Realtime → TURN). Any other provider: --ice-servers-command ./mint-turn.sh.

Behind a relay

A robot with no inbound ports dials out to a zenoh-gateway-relay on a public host, which pulls each camera once and fans it out to the browsers:

zenoh-gateway --zenoh-config robot.json5 --connect tls/relay.example.com:7447 \
    --zenoh-signalling robot --no-http --auth-file relay-token.json5

The relay (zenoh-gateway-relay --backend-name robot --backend-token <token> ...) signals over that zenoh link; the token file holds the relay's token (its grant bounds every viewer of the relay). robot.json5 carries the TLS CA and zenoh user/password for the relay's router; the WebRTC media then flows from the robot to the relay's address. tests/zenoh_signalling.rs runs this command with --no-http and connects to it the relay's way.

Building with nix

nix build .#zenoh-gateway --max-jobs auto                  # native (default package); result/bin/zenoh-gateway
nix build .#zenoh-gateway-aarch64-linux --max-jobs auto    # on a Mac: aarch64 Linux binary (Jetson, Pi 5)
nix build .#zenoh-gateway-x86_64-linux --max-jobs auto     # on a Mac: x86_64 Linux binary
nix build .#zenoh-gateway-x86_64-darwin                    # on an Apple Silicon Mac: Intel macOS binary
nix develop                                            # Rust (+ Linux targets), crate2nix, deno
  • Built with zenoh-gateway's lib.crossRust (crate2nix): every crate is its own derivation, shared with the zenoh-gateway, codecs and relay flakes (one build of zenoh, webrtc, tokio, ... for all of them). --max-jobs auto lets nix build crates in parallel.
  • The Linux builds are cross compiled with zig as the C compiler and linker (openh264, zstd, ring) against glibc 2.35 (Ubuntu 22.04, Jetson L4T 36, Pi OS bookworm): no VM, no GCC cross toolchain. GStreamer (the Jetson's hardware encoder) is opened at runtime, so nothing links it.
  • The macOS binary links /usr/lib/libiconv.2.dylib (rewritten from nix's copy), so it runs on Macs without nix. The Intel one is a plain cargo build by the same clang/SDK with --target x86_64-apple-darwin (nixpkgs no longer has x86_64-darwin, so it can't go through crate2nix).
  • After changing Cargo.lock, regenerate Cargo.nix: nix run github:dimensionalOS/zenoh-gateway#crate2nix -- generate.

Releases

All four release binaries are built on an Apple Silicon Mac, with no remote builders:

nix build .#release --builders ''   # result/<target-triple>/zenoh-gateway for
                                    # aarch64-apple-darwin, x86_64-apple-darwin,
                                    # aarch64-unknown-linux-gnu, x86_64-unknown-linux-gnu
gh release create v<version> result/dist/*  # the tarballs + SHA256SUMS

result/dist/ holds zenoh-gateway-<version>-<target-triple>.tar.gz (binary + README.md) and SHA256SUMS, made by GNU tar inside the build (no macOS extended attributes, fixed owner and mtime); install.sh reads those names.

Tests

cargo test && cargo clippy --all-targets   # the command and the test programs
deno task e2e                              # every end-to-end suite (several minutes)

The suites build zenoh-gateway and zenoh-dimos-codecs at the commits Cargo.lock pins (their checkouts come from cargo metadata: the browser client is bundled from them and the fixtures read from the codecs'). To test local changes, point cargo at your clones, e.g. cargo update after adding to .cargo/config.toml:

[patch."https://github.com/dimensionalOS/zenoh-gateway"]
zenoh-gateway = { path = "../zenoh-gateway/gateway" }
[patch."https://github.com/dimensionalOS/zenoh-dimos-codecs"]
zenoh-dimos-codecs = { path = "../zenoh-dimos-codecs" }

Each suite starts a real zenoh test peer (examples/test_peer.rs), the server, and its own headless Chrome (never the one on port 9222):

  • test/e2e.js: pipe, delivery modes, option rejection, clock sync, deadman, zenoh access control, chunked messages, compress on a raw topic (zstd byte-exact in fewer bytes, "none"), topic listing.
  • test/codecs.js: every fixture through each message encoding: depth values exact at full and half resolution (zstd by default and with compress: "none", which sends more bytes), point clouds within the documented quantization bound (intensity exact), video by its quadrant colors within ±10 of the pattern (H.264 is lossy), plus unknown-encoding and video-zstd rejections and encodes shared across frontends.
  • test/custom_codec.js: examples/custom_codec.rs (zenoh-gateway with its own zenoh session, three encodings and a video encoder of its own): a data encoding's text exact through a registerEncoding decoder (full and half encodeOptions.quality); a video encoding's I420 frames by their color within ±20, once on video-h264 (the gateway's encoder) and once on video-av1 through the application's own AV1 encoder (rav1e, registered with ServerBuilder::video_encoder as a hardware one would be), checking Chrome's negotiated format; an audio encoding's 400 Hz tone, Opus on an audio track, found by a WebAudio analyser; unknown names rejected.
  • test/allocation.js: streams shrinking by bandwidthPriority (equal, a higher priority keeping more, priority 0 first) and the quality/Hz tradeoff under --max-bandwidth-bytes-per-sec.
  • test/abandoned.js: a viewer whose browser freezes without closing anything: the gateway drops it and goes idle.
  • test/latency.js: a strict-priority stream's p99 under bulk load through a userspace UDP shaper.
  • test/throughput.js (deno task e2e:throughput): one data channel's delivered rate through the shaper with delay jitter (test/shaped_link.js): at least 2 Mb/s on a ~50 ms ± 20 ms link, and reported for a Wi-Fi-like link (RTT 10-430 ms) and a spiky, lossy one.
  • test/video_latency.js (deno task e2e:video-latency): publish -> arrival -> shown for H.264, frames identified by a send-time stamp the test peer draws into the pixels; H.264 must be shown within 10 ms of arriving (zero playout delay). --profile jitter50|wifi shapes it.
  • test/auth.js (deno task e2e:auth): --auth-file tokens (no token and unknown tokens refused, read-only can subscribe but not publish, narrower grants for subscribe/get/listTopics), revoking by editing the file, leases (another client's puts dropped with a reason, released when the holder's heartbeat stops, at maxSeconds, force-expire only with the right), and --ice-server/--udp-ports reaching both ends. With coturn's turnserver on PATH (or TURNSERVER=<path>; nixpkgs marks it broken on darwin, NIXPKGS_ALLOW_BROKEN=1 nix build --impure nixpkgs#coturn builds it) it also runs a relay-only connection through a local coturn with --turn-secret credentials.
  • test/example.js (deno task e2e:example): the example page served from the built web root, driven through its form; checks decoded video frames, drawn points and depth, the raw rate, a control re-subscribing, no console errors, and saves a screenshot to its artifacts dir (UPDATE_SCREENSHOT=1 refreshes test/artifacts/example.png).

About

The zenoh-web command: zenoh-web with the ROS 2 / dimos codecs

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages