Meshcat port forwarding with VS Code

174 views Asked by At

I'm running pydrake on a remote machine and trying to view the Meshcat visualizer GUI through port forwarding in VS Code. When I open the url in my browser, instead of seeing the Meshcat GUI, I just see raw HTML code:

<!DOCTYPE html>
<!-- This file is forked from dist/index.html in meshcat-dev/meshcat.-->
<html>

<head>
  <meta charset=utf-8>
  <title>Drake MeshCat</title>
</head>

<body>
  <div id="status-message">
    No connection to server.
  </div>
  <div id="meshcat-pane">
  </div>

  <script type="text/javascript" src="meshcat.js"></script>
  <script type="text/javascript" src="stats.min.js"></script>
  <script>
    // TODO(#16486): add tooltips to Stats to describe chart contents
    var stats = new Stats();
    var realtimeRatePanel = stats.addPanel(
            new Stats.Panel('rtr%', '#ff8', '#221')
    );
    document.body.appendChild(stats.dom);
    stats.dom.id = "stats-plot";
    // We want to show the realtime rate panel by default
    // it is the last element in the stats.dom.children list
    stats.showPanel(stats.dom.children.length - 1)
    var latestRealtimeRate = 0;
    var viewer = new MeshCat.Viewer(document.getElementById("meshcat-pane"));
    viewer.animate = function() {
      viewer.animator.update();
      if (viewer.needs_render) {
        viewer.render();
      }
    }

    const clamp = (num, min, max) => Math.min(Math.max(num, min), max);

    // Gamepad support - first check if the gamepad feature is available.
    const gamepads_supported = !!navigator.getGamepads;
    if (!gamepads_supported) {
      if (!window.isSecureContext) {
        console.warn("Gamepads are not supported outside of a secure context. "
        + "See https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts"
        + " for details. Some browsers support localhost and allowlists.");
      } else {
        console.warn("Gamepads are not supported in this browser session.");
      }
    }
    // See https://beej.us/blog/data/javascript-gamepad/ for a tutorial.
    var last_gamepad = {};
    function handle_gamepads() {
      let gamepads = navigator.getGamepads();
      let gamepad = {};
      for (let i = 0; i < gamepads.length; i++) {
        if (gamepads[i] === null || !gamepads[i].connected) {
          continue;
        }

        // Only send a subset of the available information. Also, the floating
        // point values are not constant; they are constantly changing in the
        // least significant digits even when the gamepad is untouched by the
        // user. We truncate the floating point values to two significant
        // digits to reject this noise.
        gamepad = {
          'index': gamepads[i].index,
          'button_values': gamepads[i].buttons.map(
            a => clamp(Math.round(a.value * 100) / 100, 0, 1)),
          'axes': gamepads[i].axes.map(
            a => clamp(Math.round(a * 100) / 100, -1, 1)),
        };
        break;  // Just take the first connected gamepad.
      }
      if (this.connection && this.connection.readyState == WebSocket.OPEN &&
        JSON.stringify(gamepad) !== JSON.stringify(last_gamepad)) {
        this.connection.send(MeshCat.msgpack.encode(
          { 'type': 'gamepad', 'name': '', 'gamepad': gamepad }));
        last_gamepad = gamepad;
      }
    }

    function animate() {
      stats.begin();
      // convert realtime rate to percentage so it is easier to read
      realtimeRatePanel.update(latestRealtimeRate*100, 100);
      viewer.animate()
      stats.end();
      if (gamepads_supported) {
        handle_gamepads();
      }
      requestAnimationFrame(animate);
    }

    // TODO(#16486): Replace this function with more robust custom command
    //  handling in Meshcat
    function handle_message(ws_message) {
      let decoded = viewer.decode(ws_message);
      if (decoded.type == "realtime_rate") {
        latestRealtimeRate = decoded.rate;
      } else if (decoded.type == "show_realtime_rate") {
        stats.dom.style.display = decoded.show ? "block" : "none";
      } else {
        viewer.handle_command(decoded)
      }
    }

    requestAnimationFrame( animate );
    // Set background to match the legacy ``drake_visualizer`` application of
    // days past.
    viewer.set_property(['Background'], "top_color", [.95, .95, 1.0])
    viewer.set_property(['Background'], "bottom_color", [.32, .32, .35])
    // Set the initial view looking up the y-axis.
    viewer.set_property(['Cameras', 'default', 'rotated', '<object>'],
                        "position", [0.0, 1.0, 3.0])

    <!-- CONNECTION BLOCK BEGIN -->
    // The lifespan of the server may be much shorter than this visualizer
    // client. We'd like the user to not have to explicitly reload when they
    // start a new server. So, we automatically try to reconnect at some given
    // rate. However, due to the split of visualizer state between server and
    // client, simply reconnecting may leave the *existing* visualizer in a
    // strange state with various stale artifacts. So, when we detect a
    // *reconnection*, we simply reload the page, so that every *meaningful*
    // connection is accompanied by a fresh client state. Upon loading the
    // page, it can accept a connection. After that first connection, any
    // new connection is interpreted as a signal to reload.
    var accepting_connections = true;
    status_dom = document.getElementById("status-message");
    // When the connection closes, try creating a new connection automatically.
    function make_connection(url, reconnect_ms) {
      try {
        connection = new WebSocket(url);
        connection.binaryType = "arraybuffer";
        connection.onmessage = (msg) => handle_message(msg);
        connection.onopen = (evt) => {
          if (!accepting_connections) location.reload();
          accepting_connections = false
        };
        connection.onclose = function(evt) {
          status_dom.style.display = "block";
          if (do_reconnect) {
            // Immediately schedule an attempt to reconnect.
            setTimeout(() => {make_connection(url, reconnect_ms);}, reconnect_ms);
          }
        }
        viewer.connection = connection
      } catch (e) {
        console.info("Not connected to MeshCat websocket server: ", e);
        if (do_reconnect) {
          setTimeout(() => {make_connection(url, reconnect_ms);}, reconnect_ms);
        }
      }
    }

    const queryString = window.location.search;
    const urlParams = new URLSearchParams(queryString);
    reconnect_ms = parseInt(urlParams.get('reconnect_ms')) || 1000;
    var do_reconnect = reconnect_ms > 0;
    if (do_reconnect) {
      status_dom.textContent = "No connection to server. Attempting to reconnect...";
    }

    url = location.toString();
    url = url.replace("http://", "ws://")
    url = url.replace("https://", "wss://")
    url = url.replace("/index.html", "/")
    url = url.replace("/meshcat.html", "/")
    make_connection(url, reconnect_ms);
    <!-- CONNECTION BLOCK END -->

  </script>

  <style>
    body {
      margin: 0;
    }

    #meshcat-pane {
      width: 100vw;
      height: 100vh;
      overflow: hidden;
    }

    #status-message{
      width: 50vw;
      text-align: center;
      font-weight: bold;
      background-color: yellow;
      position: fixed;
      left: 25%;
      display: none;
    }

    #stats-plot {
      display: none;
    }
  </style>
  <script id="embedded-json"></script>
</body>

</html>

I'm wondering if the port forwarding implementation in VS Code has bandwidth limitations that are preventing the Meshcat GUI from being displayed properly.

How can I solve this?

I find using ngrok for port forwarding works. The issue may be with vscode.

1

There are 1 answers

2
jwnimmer-tri On

That doesn't quite seem like a bandwidth problem. Rather, it looks more like the content-type (MIME type) is not being heuristically inferred. Maybe Drake needs to send a "content-type: text/html" in its response.

Possibly https://github.com/RobotLocomotion/drake/pull/20237 will help.