Webview and JavaScript SDK

Last validated: 2026-05-04

Oden’s WebView plugin lets you render a browser-based operator interface inside OdenVR / Player. The page can place Oden video streams into DOM-defined regions, exchange JSON messages with Oden plugins, and participate in Fleet and OCP operator workflows.

Use the webview path when the operator UI is naturally a web app: dashboards, multi-camera layouts, fleet selectors, maps, mission controls, or application-specific controls. The web page does not render Oden video pixels itself. Instead, it tells Oden where video should appear, and Oden draws the video over the browser surface.

How it fits together

Web app in Oden WebView
  -> injected OdenLayoutClient
  -> local WebSocket layout server
  -> WebView plugin
  -> Oden video layout, Fleet, OCP, or custom plugins

The WebView plugin injects a global JavaScript class named OdenLayoutClient into each page. The client opens a WebSocket to a localhost port chosen by Oden and uses the oden_layout WebSocket subprotocol. Applications should create one shared instance and reuse it for the lifetime of the page.

The injected script also exposes window.oden_version.

Enable the webview

  1. Enable the WebView global plugin in OdenVR / Player.

  2. Open the WebView plugin panel.

  3. Set Address to your web app, for example http://localhost:3000.

  4. Enable Accept Input when the page should receive mouse and keyboard input.

  5. Use Fill View when the web UI should cover the whole viewport.

The most important WebView settings are:

Address

The URL loaded by the embedded browser.

Width / Height

The browser render size when Fill View is disabled.

Accept Input

Forwards mouse and keyboard input to the page.

Inhibit Mouse Look

Prevents Oden’s 3D mouse-look behavior while interacting with the page.

Fill View

Uses the current viewport size as the browser size.

Ignore Url Updates

Keeps the currently rendered page instead of reloading when the configured URL value changes. Use this when an operator web app manages its own route state and should not be reset by project-side URL updates.

Mute log

Stops browser console logs from being forwarded into the Oden log.

Clear Session Cookies

Clears browser session cookies at startup.

Create the client

Guard the client creation so the app can still render during server-side rendering, tests, or ordinary browser development.

declare global {
  interface Window {
    OdenLayoutClient?: new () => any;
    odenLayoutClient?: any;
    oden_version?: string;
  }
}

export function getOrCreateOdenLayoutClient() {
  if (typeof window === "undefined" || !window.OdenLayoutClient) {
    return null;
  }

  if (!window.odenLayoutClient) {
    window.odenLayoutClient = new window.OdenLayoutClient();
  }

  return window.odenLayoutClient;
}

The client queues a small number of outgoing layout and user messages until its WebSocket opens. Still, your app should treat the client as a singleton. Creating multiple instances creates multiple WebSocket connections and duplicate callbacks.

JavaScript API

registerVideo(name, element)

Registers a DOM element as the placement rectangle for an Oden video stream. The client tracks getBoundingClientRect() and sends updates when the element moves, resizes, or when video attributes change.

unregisterVideo(name)

Removes a video registration. Call this when a component unmounts or a route changes.

sendPositionUpdate()

Sends the current placement for all registered video elements. This is normally called automatically.

registerCallback(callback)

Registers a callback for available-video updates. The callback receives an object keyed by video name, for example { "Front": { width: 1920, height: 1080 } }.

unregisterCallback(callback)

Removes an available-video callback.

registerUserMessageCallback(name, callback)

Registers a callback for named user messages from Oden. The callback receives the message payload.

unregisterUserMessageCallback(name, callback)

Removes a named user-message callback.

sendNamedUserMessage(name, payload)

Sends a named JSON message to Oden. Use this for plugin messages, Fleet messages, and OCP messages.

sendMessage(layout) / sendBoxData(layout)

Sends a raw video layout message. Use this only when you need manual video placement.

close()

Closes the WebSocket, disconnects observers, and stops the position update loop.

Video layout

The recommended path is to create transparent DOM elements where video should appear, then register those elements with registerVideo(). Oden draws the corresponding stream into each rectangle.

import { useEffect, useRef } from "react";
import { getOrCreateOdenLayoutClient } from "./oden";

export function OdenVideo({
  name,
  scaleX = "1.0",
  scaleY = "1.0",
  rotation = "0.0",
  cropLeft = "0.0",
  cropRight = "0.0",
  cropTop = "0.0",
  cropBottom = "0.0",
  z = "-1",
}) {
  const ref = useRef(null);

  useEffect(() => {
    const client = getOrCreateOdenLayoutClient();
    if (!client || !ref.current) return;

    client.registerVideo(name, ref.current);

    return () => {
      client.unregisterVideo(name);
    };
  }, [name]);

  return (
    <div
      ref={ref}
      style={{ width: "100%", height: "100%", background: "transparent" }}
      scale-x={scaleX}
      scale-y={scaleY}
      rotation={rotation}
      crop-left={cropLeft}
      crop-right={cropRight}
      crop-top={cropTop}
      crop-bottom={cropBottom}
      z={z}
    />
  );
}

A stream name is usually the camera or video entity name, such as Front. When Fleet or another multi-streamer setup is active, names can include a vehicle prefix, such as vehicle-12:Front. Listen for available videos instead of hard-coding names whenever possible:

const client = getOrCreateOdenLayoutClient();

function onVideos(videos) {
  // { "Front": { width: 1920, height: 1080 }, ... }
  console.log("Available Oden videos", videos);
}

client?.registerCallback(onVideos);

// Later:
client?.unregisterCallback(onVideos);

Layout attributes

The automatic layout client reads these attributes from the registered DOM element:

scale-x, scale-y

Scale multipliers for the video placement. Use scale-x="-1.0" to mirror a rear camera.

rotation

Rotation in degrees.

crop-left, crop-right, crop-top, crop-bottom

Normalized crop values from 0.0 to 1.0.

z

View-space z order used by Oden’s immediate drawing path.

data-camera-id

Optional camera name override. Use this if the DOM registration key must be unique but the video stream name should be shared.

Manual placement

Manual placement sends the raw layout shape expected by the WebView layout server. Do not wrap video placement in sendNamedUserMessage().

const client = getOrCreateOdenLayoutClient();

client?.sendMessage({
  videos: [
    {
      name: "Front",
      position: { x: 100, y: 80 },
      size: { width: 1280, height: 720 },
      scale: { x: 1.0, y: 1.0 },
      rotation: 0.0,
      crop: { left: 0.0, right: 0.0, top: 0.0, bottom: 0.0 },
      z: 1,
    },
  ],
});

Coordinates and sizes are browser CSS pixels in the web page coordinate system. Oden applies the current webview scale internally.

Named user messages

Named user messages are JSON messages with this shape:

{
  "name": "vehicle_state",
  "payload": {
    "speed": 4.2,
    "battery": 0.81
  }
}

Use named messages for application data. Use raw layout messages for video placement.

JavaScript to Oden

const client = getOrCreateOdenLayoutClient();

client?.sendNamedUserMessage("drive_commands", {
  throttle: 0.15,
  brake: 0.0,
  steering: -0.2,
});

Oden to JavaScript

const client = getOrCreateOdenLayoutClient();

function onVehicleState(payload) {
  console.log("Vehicle state", payload);
}

client?.registerUserMessageCallback("vehicle_state", onVehicleState);

// Later:
client?.unregisterUserMessageCallback("vehicle_state", onVehicleState);

Plugin side

Rust plugins send messages to webviews through the named MPMC channel user_message_oden_to_webview. Current inbound webview messages are received with webview_shared::create_receiver() and a set of wanted message names.

use std::collections::HashSet;

use oden_plugin_rs::{
    named_mpmc_channel::{NamedMpmcChannelExt, Sender},
    webview_user_message::WebviewUserMessage,
    InitParams, OdenPlugin, UpdateParams,
};
use serde_json::json;

struct State {
    to_webview: Sender<WebviewUserMessage>,
    from_webview: webview_shared::Receiver,
}

impl OdenPlugin for State {
    fn init(api: &InitParams) -> Self {
        let wanted_messages = HashSet::from(["drive_commands".to_string()]);

        Self {
            to_webview: api.named_mpmc_channel_tx("user_message_oden_to_webview", 128),
            from_webview: webview_shared::create_receiver(api, wanted_messages),
        }
    }

    fn update(&mut self, _api: &UpdateParams) {
        self.to_webview
            .try_send(WebviewUserMessage {
                name: "vehicle_state".to_string(),
                payload: Some(json!({
                    "speed": 4.2,
                    "battery": 0.81
                })),
            })
            .ok();

        while let Ok(message) = self.from_webview.try_recv() {
            if message.name == "drive_commands" {
                // Deserialize message.payload into your command type.
            }
        }
    }
}

webview_shared::create_receiver() filters by message name. If a message name is not in wanted_messages, that plugin instance will not receive it.

Fleet messages

When the Fleet plugin is active, webviews can discover and connect vehicles with named user messages. These are the message names used by the current Fleet player plugin.

Direction Name Payload

Webview to Oden

fleet_list_vehicles

{}

Oden to webview

fleet_online_vehicles

Array of { id, name, availability }, where availability is Available, Monitored, or Controlled.

Webview to Oden

fleet_connect

{ "vehicle_name": "vehicle-12" }

Webview to Oden

fleet_disconnect

{ "vehicle_name": "vehicle-12" }

Webview to Oden

configure_player

Optional player configuration such as packer_enabled, packer_auto_crop, background_color, or hide_connection_ui.

Example:

const client = getOrCreateOdenLayoutClient();

function onVehicles(vehicles) {
  // [{ id, name, availability }, ...]
  console.log(vehicles);
}

client?.registerUserMessageCallback("fleet_online_vehicles", onVehicles);
client?.sendNamedUserMessage("fleet_list_vehicles", {});

client?.sendNamedUserMessage("fleet_connect", {
  vehicle_name: "vehicle-12",
});

OCP operator messages

OCP publishes vehicle feedback to the webview and accepts optional operator-side user data back from the page. See Oden Control Pipeline overview for the full control-path model.

Direction Name Payload

Oden to webview

ocp_vehicle_user_data

VehicleOcpShared: { vehicle_feedback, last_input }.

Webview to Oden

ocp_client_user_data

ClientOcpShared: { active_vehicle, client_user_data }.

vehicle_feedback is keyed by vehicle name. Each value includes OCP latency fields, sender and receiver faults, optional vehicle data, the last remote input, and timestamp fields used by the OCP latency loop.

When JavaScript sends ocp_client_user_data, the injected client automatically echoes the latest ack_time and ack_time_mac for each vehicle entry it has seen. Your web app normally only supplies active_vehicle, user_data, and optionally ocp_disable_gamepad.

const client = getOrCreateOdenLayoutClient();

function onOcpData(payload) {
  const entries = Object.entries(payload.vehicle_feedback ?? {});
  if (entries.length === 0) return;

  const [vehicleName] = entries[0];

  client?.sendNamedUserMessage("ocp_client_user_data", {
    active_vehicle: vehicleName,
    client_user_data: {
      [vehicleName]: {
        user_data: {
          selected_tool: "bucket",
          requested_mode: "work",
        },
        ocp_disable_gamepad: false,
      },
    },
  });
}

client?.registerUserMessageCallback("ocp_vehicle_user_data", onOcpData);

Only one operator-side client data source should send OCP client data in a session. Do not send ocp_client_user_data from both a webview and another operator-side TCP or plugin client at the same time.

Message names reference

Name Direction from web page Use

fleet_list_vehicles

Send

Request the current Fleet vehicle list.

fleet_online_vehicles

Receive

Receive Fleet vehicle availability.

fleet_connect

Send

Request control connection by vehicle name.

fleet_disconnect

Send

Disconnect a Fleet vehicle by vehicle name.

configure_player

Send

Apply selected player-side configuration.

ocp_vehicle_user_data

Receive

Receive OCP vehicle feedback and input state.

ocp_client_user_data

Send

Send operator-side user data and active vehicle selection to OCP.

connect

Send

Low-level connection request with { "roverId": "…​" }. Fleet integrations should prefer fleet_connect.

disconnect

Send

Low-level disconnect request with { "roverId": "…​" }. Fleet integrations should prefer fleet_disconnect.

Custom plugins can define any additional message names. Use stable, namespaced names such as vehicle_state, drive_commands, or my_plugin:set_mode.

Troubleshooting

No OdenLayoutClient

The page is not running inside Oden’s WebView, or the injected script did not load. Keep the singleton helper nullable so local browser development still works.

No video appears

Check Layout Server > Available videos in the WebView plugin panel, or use registerCallback() to log available names. Make sure the registered element has non-zero size and the name matches the stream name Oden reports.

Video is behind the web UI

Adjust the element’s z attribute. For overlays and picture-in-picture views, use an explicit positive z.

Plugin does not receive webview messages

Make sure the plugin registered the message name in webview_shared::create_receiver(api, wanted_messages).

OCP client data stops working

Only one OCP client data source can own operator-side user data in a session. Restart Oden after a source conflict, then send from only one of webview, plugin shared data, or player-side TCP.