Webview and JavaScript SDK
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
-
Enable the
WebViewglobal plugin in OdenVR / Player. -
Open the WebView plugin panel.
-
Set
Addressto your web app, for examplehttp://localhost:3000. -
Enable
Accept Inputwhen the page should receive mouse and keyboard input. -
Use
Fill Viewwhen 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 Viewis 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.0to1.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 |
|
|
Oden to webview |
|
Array of |
Webview to Oden |
|
|
Webview to Oden |
|
|
Webview to Oden |
|
Optional player configuration such as |
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 |
|
|
Webview to Oden |
|
|
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 |
|---|---|---|
|
Send |
Request the current Fleet vehicle list. |
|
Receive |
Receive Fleet vehicle availability. |
|
Send |
Request control connection by vehicle name. |
|
Send |
Disconnect a Fleet vehicle by vehicle name. |
|
Send |
Apply selected player-side configuration. |
|
Receive |
Receive OCP vehicle feedback and input state. |
|
Send |
Send operator-side user data and active vehicle selection to OCP. |
|
Send |
Low-level connection request with |
|
Send |
Low-level disconnect request with |
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 videosin the WebView plugin panel, or useregisterCallback()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
zattribute. For overlays and picture-in-picture views, use an explicit positivez. - 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.