diff --git a/docs/EMBEDDING.md b/docs/EMBEDDING.md
index 535a229..41c3dbc 100644
--- a/docs/EMBEDDING.md
+++ b/docs/EMBEDDING.md
@@ -249,6 +249,109 @@ module.exports = {
};
```
+### Next.js (App Router / Turbopack)
+
+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`**
+
+```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:
+
+```tsx
+// 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 (
+
+
+
+ );
+}
+```
+
+```tsx
+// 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 ;
+}
+```
+
+**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:
+
+```ts
+// 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`:** ROSView does **not** support remote streaming of db3 (SQLite needs random access to the whole database). Download the db3 file in full as a `File` on the client and pass it via the `file` / `files` prop. mcap / bag can be streamed directly through the Range route above.
+
+> **Version requirement:** Use **`@ioai/rosview` ≥ 1.3.5** (which depends on `@ioai/wasm-zstd` ≥ 1.1.2). 1.3.5 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 a `blob:` URL and unable to resolve the zstd glue's relative dynamic import). `@ioai/wasm-zstd` 1.1.2 statically inlines the glue into the worker, fixing this for good.
+
---
## Troubleshooting
@@ -281,6 +384,10 @@ Access-Control-Allow-Origin: https://your-app.example.com
Firefox has stricter CSP enforcement for workers. If you use a `Content-Security-Policy` header, add `worker-src 'self' blob:`.
+### "Failed to resolve module specifier './wasm-zstd-*.js'"
+
+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.
+
---
## TypeScript support
diff --git a/docs/EMBEDDING.zh.md b/docs/EMBEDDING.zh.md
index cfe1622..c800a21 100644
--- a/docs/EMBEDDING.zh.md
+++ b/docs/EMBEDDING.zh.md
@@ -249,6 +249,109 @@ module.exports = {
};
```
+### Next.js(App Router / Turbopack)
+
+ROSView 是纯浏览器端组件(依赖 Web Worker、WASM、`window` 等),**不能在服务端渲染**。在 Next.js 中集成时遵循以下三点即可:
+
+**1. 在 `next.config.js` 中转译本包**
+
+```js
+// next.config.js
+const nextConfig = {
+ transpilePackages: ['@ioai/rosview'],
+};
+
+module.exports = nextConfig;
+```
+
+**2. 用客户端组件包裹,并以 `ssr: false` 动态加载**
+
+`ssr: false` 只能在**客户端组件**里使用,不能直接在服务端组件(如 `page.tsx`)里对 `next/dynamic` 使用。因此请新建一个 `'use client'` 包裹组件:
+
+```tsx
+// components/RosViewerClient.tsx
+'use client';
+
+import '@ioai/rosview/style.css';
+import dynamic from 'next/dynamic';
+
+// 仅在客户端加载,避免 SSR 阶段触碰浏览器 API
+const RosViewer = dynamic(
+ () => import('@ioai/rosview').then((m) => ({ default: m.RosViewer })),
+ { ssr: false },
+);
+
+export function RosViewerClient({ url }: { url: string }) {
+ return (
+
+
+
+ );
+}
+```
+
+```tsx
+// app/visualize/page.tsx —— 服务端组件直接渲染上面的客户端组件
+import { RosViewerClient } from '@/components/RosViewerClient';
+
+export default async function VisualizePage({
+ searchParams,
+}: {
+ searchParams: Promise<{ url?: string }>;
+}) {
+ const { url } = await searchParams;
+ return ;
+}
+```
+
+**3.(可选)提供支持 HTTP Range 的文件接口**
+
+要流式可视化本地/私有的 mcap/bag,可在 Next.js 中提供一个支持 `Accept-Ranges` 的 GET 路由,把 `url` 指向它即可:
+
+```ts
+// 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'; // 经过校验的安全路径
+ const { size } = await fs.promises.stat(filePath);
+ const range = req.headers.get('range');
+
+ // 无 Range:返回整文件,但仍声明支持 Range
+ 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',
+ },
+ });
+}
+```
+
+> **关于 `db3`**:ROSView **不支持远程流式** db3(SQLite 需随机读取整库)。请在前端先把 db3 整文件下载为 `File`,再通过 `file` / `files` prop 传入。mcap / bag 则可直接用上面的 Range 接口流式加载。
+
+> **版本要求**:请使用 **`@ioai/rosview` ≥ 1.3.5**(依赖 `@ioai/wasm-zstd` ≥ 1.1.2)。1.3.5 修复了 1.3.4 在 Turbopack 下因 inline worker 以 `blob:` URL 运行、无法解析 zstd glue 相对动态 import 而抛出的 `Failed to resolve module specifier './wasm-zstd-*.js'`;`@ioai/wasm-zstd` 1.1.2 将 glue 静态内联进 worker,彻底解决该问题。
+
---
## 故障排查
@@ -281,6 +384,10 @@ Access-Control-Allow-Origin: https://your-app.example.com
Firefox 对 Worker 的 CSP 更严格。若使用 `Content-Security-Policy` 响应头,请添加 `worker-src 'self' blob:`。
+### "Failed to resolve module specifier './wasm-zstd-*.js'"
+
+该错误出现在 `@ioai/rosview` 1.3.4(搭配 `@ioai/wasm-zstd` 1.1.1)在以 `blob:` URL 运行 inline worker 的打包器(如 Next.js Turbopack)下。升级到 **`@ioai/rosview` ≥ 1.3.5**(`@ioai/wasm-zstd` ≥ 1.1.2)即可修复——新版本会将 zstd glue 静态内联进 worker,不再有运行时相对动态 import。
+
---
## TypeScript 支持
diff --git a/package-lock.json b/package-lock.json
index 517c360..c6be19a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@ioai/rosview",
- "version": "1.3.4",
+ "version": "1.3.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@ioai/rosview",
- "version": "1.3.4",
+ "version": "1.3.5",
"license": "MIT",
"devDependencies": {
"@eslint/js": "^9.39.4",
@@ -20,7 +20,7 @@
"@foxglove/rosmsg-serialization": "^2.0.4",
"@foxglove/rosmsg2-serialization": "^3.0.3",
"@ioai/hdf5": "^1.0.0",
- "@ioai/wasm-zstd": "^1.1.1",
+ "@ioai/wasm-zstd": "^1.1.2",
"@mcap/browser": "^1.1.0",
"@mcap/core": "^2.0.2",
"@playwright/test": "^1.59.1",
@@ -1321,9 +1321,9 @@
}
},
"node_modules/@ioai/wasm-zstd": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@ioai/wasm-zstd/-/wasm-zstd-1.1.1.tgz",
- "integrity": "sha512-9uWUW3Rp2PDUYzqZ0p4qU9o2uPXHBZYVuWabVdFpX+W2hmfdOWH+nxgl8f7VkFjnonj8nRebaJPd6BouXjaRPw==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@ioai/wasm-zstd/-/wasm-zstd-1.1.2.tgz",
+ "integrity": "sha512-YT+9O3jas5XNuHjUgyPy116oNZQbAcAATas0m6kFpX5lDlLhLHJms9qWJ+398HmUaw+PTuk6H/a2oGefMHHafg==",
"dev": true,
"license": "MIT"
},
diff --git a/package.json b/package.json
index 8ed4725..edc464d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@ioai/rosview",
- "version": "1.3.4",
+ "version": "1.3.5",
"description": "High-performance robotics data visualization for MCAP, ROS bag, ROS2 db3, HDF5 and BVH — embeddable React component and standalone SPA",
"keywords": [
"ros",
@@ -97,7 +97,7 @@
"@foxglove/rosmsg-serialization": "^2.0.4",
"@foxglove/rosmsg2-serialization": "^3.0.3",
"@ioai/hdf5": "^1.0.0",
- "@ioai/wasm-zstd": "^1.1.1",
+ "@ioai/wasm-zstd": "^1.1.2",
"@mcap/browser": "^1.1.0",
"@mcap/core": "^2.0.2",
"@playwright/test": "^1.59.1",