Simplified Chinese: EMBEDDING.zh.md
This guide walks through integrating ROSView as an embeddable React component into your own application.
- React ≥ 19 and react-dom ≥ 19
- A bundler that supports ES modules and Web Workers (Vite recommended; Webpack 5+ works with configuration)
- Node.js ≥ 22 (matches
enginesinpackage.json; CI uses Node 24)
npm install @ioai/rosviewThe component requires its bundled CSS. Import it once at your application root:
// main.tsx or App.tsx (top-level entry point)
import '@ioai/rosview/style.css';Styles are scoped to #rosview-root inside the bundle, so Tailwind preflight does not reset global elements (navbar, buttons, etc.) in your host application.
import { RosViewer } from '@ioai/rosview';
export default function App() {
return (
<div style={{ width: '100%', height: '100vh' }}>
<RosViewer
url="https://cdn.example.com/recording.mcap"
theme="dark"
language="en"
/>
</div>
);
}The component defaults to height: 100vh. Use the className or style props to constrain its dimensions when embedding within a larger page.
By default urlState is 'off': the viewer does not call history.pushState or interpret file:// / folder:// in the url prop as restorable locators. Use urlState="spa" only if you are building a full-page app that owns the address bar (same behavior as the standalone site).
import React from 'react';
import { RosViewer } from '@ioai/rosview';
export function FileViewer() {
const [file, setFile] = React.useState<File>();
return (
<div style={{ height: '100vh' }}>
{!file && (
<input
type="file"
accept=".mcap,.bag,.db3,.h5,.hdf5,.bvh"
onChange={e => setFile(e.target.files?.[0])}
/>
)}
{file && (
<RosViewer
file={file}
theme="system"
onFatalError={err => alert(`Failed to load: ${err.message}`)}
/>
)}
</div>
);
}For data portals or review dashboards with many recordings, serve a JSON manifest:
[
{
"url": "https://cdn.example.com/session_001.mcap",
"name": "Session 001 — Parking lot",
"sizeBytes": 2147483648,
"durationSec": 180,
"topicCount": 24
},
{
"url": "https://cdn.example.com/session_002.mcap",
"name": "Session 002 — Highway",
"sizeBytes": 1073741824,
"durationSec": 90
}
]Then pass the manifest URL:
<RosViewer
fileManifest="https://cdn.example.com/manifest.json"
theme="dark"
/>Or pass the parsed rows directly (if you already fetched and filtered them):
import { parseRemoteDatasetListJson } from '@ioai/rosview';
const res = await fetch('/api/datasets');
const rows = parseRemoteDatasetListJson(await res.json());
<RosViewer fileManifest={rows} />The navbar shows ROS View on the left by default. When embedding inside a larger app, you can hide it or replace it with your product name:
// Hide the brand button (File / Layout menus remain)
<RosViewer url="..." showNavbarBrand={false} />
// Replace with host branding
<RosViewer url="..." navbarBrandLabel="Acme Data Portal" />Related props:
| Prop | Description |
|---|---|
showNavbarBrand |
Left brand button visibility (true by default). |
navbarBrandLabel |
Custom brand text; defaults to the localized product name. |
navbarSourceName |
Center label for the active dataset (separate from brand). |
showNavbar |
Hides the entire navbar when false (via chrome or explicit prop). |
Disable internal localStorage persistence and fully control state from your application:
import { RosViewer } from '@ioai/rosview';
import { readPreferences, writePreferences } from '@ioai/rosview';
function ControlledViewer() {
const saved = readPreferences();
const [theme, setTheme] = React.useState(saved?.theme ?? 'system');
const [lang, setLang] = React.useState(saved?.language ?? 'en');
const handleThemeChange = (t: typeof theme) => {
setTheme(t);
writePreferences({ theme: t !== 'system' ? t : undefined });
};
return (
<RosViewer
url="https://cdn.example.com/recording.mcap"
theme={theme}
language={lang}
preferencePersistence="off"
onThemeChange={handleThemeChange}
onLanguageChange={setLang}
/>
);
}If your team uses Foxglove Studio layouts, you can import them directly:
import { importFoxgloveLayout } from '@ioai/rosview';
// layout is the JSON object from a .json Foxglove layout file
const result = importFoxgloveLayout(layout);
if (result.success) {
console.log('Imported panels:', result.panelCount);
} else {
console.warn('Import failed:', result.error);
}import { RosViewer, useAnnotationController } from '@ioai/rosview';
function AnnotationApp() {
const controller = useAnnotationController({
dictionary: {
skills: [
{ id: 'pick', label: 'Pick object' },
{ id: 'place', label: 'Place object' },
],
},
onAnnotationsChange: ({ annotations }) => {
console.log('Updated annotations:', annotations.length);
},
onExport: async (payload) => {
await fetch('/api/annotations', {
method: 'POST',
body: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' },
});
},
});
return (
<RosViewer
url="https://cdn.example.com/recording.mcap"
theme="dark"
/>
);
}No special configuration needed. Worker imports (?worker) are handled by Vite automatically.
// vite.config.ts — no ROSView-specific config required
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
});Enable WebAssembly and Worker support:
// webpack.config.js
module.exports = {
experiments: {
asyncWebAssembly: true,
},
};ROSView is a browser-only component (it relies on Web Workers, WASM, window, etc.) and cannot be server-rendered. Integrating it into Next.js takes three steps:
1. Transpile the package in next.config.js
// next.config.js
const nextConfig = {
transpilePackages: ['@ioai/rosview'],
};
module.exports = nextConfig;2. Wrap it in a Client Component loaded with ssr: false
ssr: false is only allowed inside a Client Component — you cannot use it with next/dynamic directly in a Server Component (e.g. page.tsx). So create a 'use client' wrapper:
// components/RosViewerClient.tsx
'use client';
import '@ioai/rosview/style.css';
import dynamic from 'next/dynamic';
// Client-only load to avoid touching browser APIs during SSR
const RosViewer = dynamic(
() => import('@ioai/rosview').then((m) => ({ default: m.RosViewer })),
{ ssr: false },
);
export function RosViewerClient({ url }: { url: string }) {
return (
<div style={{ width: '100%', height: '100vh' }}>
<RosViewer url={url} theme="dark" />
</div>
);
}// app/visualize/page.tsx — a Server Component rendering the client wrapper
import { RosViewerClient } from '@/components/RosViewerClient';
export default async function VisualizePage({
searchParams,
}: {
searchParams: Promise<{ url?: string }>;
}) {
const { url } = await searchParams;
return <RosViewerClient url={url ?? ''} />;
}3. (Optional) Serve files through a Range-capable route
To stream local/private mcap/bag recordings, expose a GET route that supports Accept-Ranges and point url at it:
// app/api/recording/route.ts
import fs from 'node:fs';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(req: NextRequest) {
const filePath = '/data/recording.mcap'; // validated, safe path
const { size } = await fs.promises.stat(filePath);
const range = req.headers.get('range');
// No Range: return the whole file but still advertise Range support
if (!range) {
const stream = fs.createReadStream(filePath);
return new NextResponse(stream as unknown as ReadableStream, {
status: 200,
headers: {
'Content-Type': 'application/octet-stream',
'Content-Length': String(size),
'Accept-Ranges': 'bytes',
},
});
}
const [startStr, endStr] = range.replace('bytes=', '').split('-');
const start = Number(startStr);
const end = endStr ? Number(endStr) : size - 1;
const stream = fs.createReadStream(filePath, { start, end });
return new NextResponse(stream as unknown as ReadableStream, {
status: 206,
headers: {
'Content-Type': 'application/octet-stream',
'Content-Range': `bytes ${start}-${end}/${size}`,
'Content-Length': String(end - start + 1),
'Accept-Ranges': 'bytes',
},
});
}About
db3: Like the other formats, db3 supports both a localFileand a remote URL — just pointurlat the Range route above. However, because SQLite needs random access to the whole database, db3 cannot be Range-streamed the way mcap / bag are: when given a db3 URL, ROSView downloads the file in full inside the Worker (with download progress) before opening it. For very large db3 files, prefer converting to MCAP for true streaming. You no longer need to download the db3 as aFileyourself on the host side.
Version requirement: Use
@ioai/rosview≥ 1.3.5 (which depends on@ioai/wasm-zstd≥ 1.1.2). 1.3.5 adds remote-URL loading for db3 and fixes the Turbopack inline-worker regression from 1.3.4 (Failed to resolve module specifier './wasm-zstd-*.js', caused by the inline worker running from ablob:URL and unable to resolve the zstd glue's relative dynamic import).@ioai/wasm-zstd1.1.2 statically inlines the glue into the worker, fixing this for good.
Some WASM decoders (bz2, lz4) benefit from SharedArrayBuffer. Your server must send the following headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
The component works without these headers; affected decoders fall back to non-shared memory.
Remote files must be served with CORS headers that allow your application's origin:
Access-Control-Allow-Origin: https://your-app.example.com
- Ensure the server supports HTTP Range requests (
Accept-Ranges: bytes). - Files without Range support are downloaded in full before playback can start.
Firefox has stricter CSP enforcement for workers. If you use a Content-Security-Policy header, add worker-src 'self' blob:.
This occurs with @ioai/rosview 1.3.4 (paired with @ioai/wasm-zstd 1.1.1) under bundlers that run inline workers from blob: URLs, such as Next.js Turbopack. Upgrade to @ioai/rosview ≥ 1.3.5 (@ioai/wasm-zstd ≥ 1.1.2) — newer versions statically inline the zstd glue into the worker, removing the runtime relative dynamic import.
The package ships full TypeScript declarations. Import types directly:
import type {
RosViewerProps,
FileListItem,
PreferencePersistence,
} from '@ioai/rosview';RosView ships no product-specific annotation, QC rules, or persistence. Host applications should:
- Pass opaque
hostContextonRosViewer(for example{ datasetId, canAnnotate }) and readcontext.hostContextinsideextensions. - Implement sidebar tabs and
playbackOverlays/timelineOverlaysusingcontext.playback,context.timeline, and optionallycontext.messages.getMessagesInTimeRange. - Keep all REST calls, permissions, and domain models in the host app.