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.
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.