Skip to content

Commit 47ea86b

Browse files
authored
feat: release v1.3.4
Release/1.3.4
2 parents dea320e + 9bb9b97 commit 47ea86b

31 files changed

Lines changed: 545 additions & 271 deletions

NOTICE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ react-intl
6262
Copyright 2026 IO-AI Tech
6363
Licensed under the MIT License; third-party notices ship with the npm package (see node_modules/@ioai/hdf5/THIRD_PARTY_NOTICES.md after install).
6464
https://www.npmjs.com/package/@ioai/hdf5
65-
Source: https://github.com/ioai-tech/hdf5-wasm
65+
Source: https://github.com/ioai-tech/wasm-hdf5
6666

6767
fflate
6868
Copyright 2020-2024 Arjun Barrett

docs/ARCHITECTURE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,8 @@ Fixed-width sidebar (collapsible) on the left with three tabs:
283283
| Library | Purpose |
284284
|---------|---------|
285285
| fflate | General compression/decompression (gzip/deflate/zlib) |
286-
| fzstd | Zstandard decompression (MCAP chunk compression) |
287-
| lz4js / lz4-wasm | LZ4 decompression (ROS1 `.bag` chunk compression) |
286+
| @ioai/wasm-zstd | Vite-friendly WebAssembly Zstandard decompression for MCAP chunk compression |
287+
| lz4js | Browser-safe LZ4 decompression for MCAP/ROS1 chunk compression |
288288

289289
---
290290

@@ -358,7 +358,7 @@ interface Readable {
358358
}
359359

360360
// Local file
361-
class BlobReadable implements Readable { ... }
361+
// Uses @mcap/browser BlobReadable and adapts it to the MCAP IReadable API.
362362

363363
// Remote file (HTTP Range + LRU cache)
364364
class CachedFilelike implements Readable {
@@ -809,7 +809,7 @@ rosview/
809809
└── infra/
810810
├── workers/ # mcap/bag/db3/hdf5 workers and transport
811811
├── sources/ # IterableSource implementations
812-
└── services/ # HttpFileReader, CachedFilelike, BlobReadable
812+
└── services/ # HttpFileReader, CachedFilelike
813813
```
814814

815815
---

docs/ARCHITECTURE.zh.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,8 @@
276276
|| 说明 |
277277
|----|------|
278278
| fflate | 通用压缩/解压(gzip/deflate/zlib) |
279-
| lz4-wasm 或 lz4js | LZ4 解压(ROS1 .bag chunk 压缩) |
280-
| fzstd | Zstandard 解压(MCAP chunk 压缩) |
279+
| @ioai/wasm-zstd | Vite 友好的 WebAssembly Zstandard 解压(MCAP chunk 压缩) |
280+
| lz4js | 浏览器安全的 LZ4 解压(MCAP/ROS1 chunk 压缩) |
281281

282282
---
283283

@@ -351,7 +351,7 @@ interface Readable {
351351
}
352352

353353
// 本地文件
354-
class BlobReadable implements Readable { ... }
354+
// 使用 @mcap/browser BlobReadable,并适配到 MCAP IReadable API。
355355

356356
// 远程文件(HTTP Range + LRU 缓存)
357357
class CachedFilelike implements Readable {
@@ -854,7 +854,7 @@ rosview/
854854
└── infra/
855855
├── workers/ # mcap/bag/db3/hdf5 worker 与传输层
856856
├── sources/ # IterableSource 与各格式实现
857-
└── services/ # HttpFileReader、CachedFilelike、BlobReadable
857+
└── services/ # HttpFileReader、CachedFilelike
858858
```
859859

860860
以下能力若在旧版树状图中出现、但上表中未列出,视为 **规划/拆分方向** 或尚未以独立文件落地,以 `git` 与 IDE 为准。
@@ -900,7 +900,9 @@ rosview/
900900
"react-intl": "^10.1.2",
901901

902902
"fflate": "^0.8.2",
903-
"fzstd": "^0.1.1",
903+
"@mcap/browser": "^1.1.0",
904+
"@ioai/wasm-zstd": "^1.1.0",
905+
"lz4js": "^0.2.0",
904906

905907
"zustand": "^5.0.0",
906908
"eventemitter3": "^5.0.1"

package-lock.json

Lines changed: 24 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ioai/rosview",
3-
"version": "1.3.3",
3+
"version": "1.3.4",
44
"description": "High-performance robotics data visualization for MCAP, ROS bag, ROS2 db3, HDF5 and BVH — embeddable React component and standalone SPA",
55
"keywords": [
66
"ros",
@@ -97,6 +97,8 @@
9797
"@foxglove/rosmsg-serialization": "^2.0.4",
9898
"@foxglove/rosmsg2-serialization": "^3.0.3",
9999
"@ioai/hdf5": "^1.0.0",
100+
"@ioai/wasm-zstd": "^1.1.1",
101+
"@mcap/browser": "^1.1.0",
100102
"@mcap/core": "^2.0.2",
101103
"@playwright/test": "^1.59.1",
102104
"@radix-ui/react-collapsible": "^1.1.12",
@@ -133,7 +135,6 @@
133135
"eventemitter3": "^5.0.1",
134136
"fflate": "^0.8.2",
135137
"flatbuffers": "^25.9.23",
136-
"fzstd": "^0.1.1",
137138
"globals": "^17.4.0",
138139
"happy-dom": "^20.9.0",
139140
"intervals-fn": "^3.0.3",

playwright.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default defineConfig({
1111
// Match vite preview host; static preview is more reliable for Range/HEAD than raw dev.
1212
baseURL: 'http://127.0.0.1:4173',
1313
trace: 'on-first-retry',
14+
screenshot: 'only-on-failure',
1415
},
1516
projects: [
1617
{

src/core/players/IterablePlayer.ts

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const TRANSPORT_DIAGNOSTICS_POLL_INTERVAL_MS = 2000;
2323
const EMPTY_BATCH_BACKFILL_COOLDOWN_MS = 1000;
2424
const EMPTY_BATCH_BACKFILL_TRIGGER = 4;
2525
const BACKFILL_STALE_THRESHOLD_NS = 1_000_000_000n;
26+
const BACKFILL_STALE_TOPIC_PERIOD_MULTIPLIER = 3;
2627
const STALE_TOPIC_REFRESH_COOLDOWN_MS = 500;
2728
const SLOW_DISTRIBUTION_MS = 16;
2829

@@ -162,6 +163,7 @@ export class IterablePlayer implements Player {
162163
private _fallbackBackfillCount = 0;
163164
private _lastFallbackBackfillMs = 0;
164165
private _lastStaleRefreshMs = 0;
166+
private _staleRefreshInFlight = false;
165167
private _isBuffering = false;
166168
private _topicLastMessageNs = new Map<string, bigint>();
167169
private _highFrequencyConsumerSignature = "";
@@ -1004,10 +1006,7 @@ export class IterablePlayer implements Player {
10041006
}
10051007
this._currentTime = nextTime;
10061008
this._clock.seek(this._currentTime, performance.now());
1007-
await this._refreshStaleTopicsFromBackfill(now, epoch);
1008-
if (!this._isPlaybackEpochCurrent(epoch)) {
1009-
return;
1010-
}
1009+
this._scheduleStaleTopicsRefresh(now, epoch);
10111010

10121011
if (this._initialization && toNano(this._currentTime) >= toNano(this._initialization.end)) {
10131012
if (this._isLooping) {
@@ -1238,7 +1237,10 @@ export class IterablePlayer implements Player {
12381237
this._cursor = cursor;
12391238
}
12401239

1241-
private async _refreshStaleTopicsFromBackfill(nowMs: number, epoch: number): Promise<void> {
1240+
private _scheduleStaleTopicsRefresh(nowMs: number, epoch: number): void {
1241+
if (this._staleRefreshInFlight) {
1242+
return;
1243+
}
12421244
if (nowMs - this._lastStaleRefreshMs < STALE_TOPIC_REFRESH_COOLDOWN_MS) {
12431245
return;
12441246
}
@@ -1250,40 +1252,56 @@ export class IterablePlayer implements Player {
12501252
const staleTopics = topics.filter((topic) => {
12511253
const lastNs = this._topicLastMessageNs.get(topic);
12521254
if (lastNs == null) return true;
1253-
return nowNs - lastNs > BACKFILL_STALE_THRESHOLD_NS;
1255+
return nowNs - lastNs > this._staleThresholdNsForTopic(topic);
12541256
});
12551257
if (staleTopics.length === 0) {
12561258
return;
12571259
}
12581260
this._lastStaleRefreshMs = nowMs;
12591261
const referenceTime = this._currentTime;
1260-
try {
1261-
const messages = await this._source.getBackfillMessages({
1262-
time: referenceTime,
1263-
topics: staleTopics,
1264-
});
1265-
if (!this._isPlaybackEpochCurrent(epoch)) {
1266-
return;
1267-
}
1268-
// Same reasoning as in _handleEmptyBatch: latched topics would otherwise
1269-
// get re-delivered on every refresh tick (~5 Hz), causing panels like
1270-
// the 3D/URDF renderer to rebuild from scratch and leak GPU buffers.
1271-
const freshMessages = this._filterAlreadyDeliveredMessages(messages);
1272-
if (freshMessages.length === 0) {
1273-
return;
1274-
}
1275-
this._distributeMessages(freshMessages, referenceTime);
1276-
if (this._debugEnabled) {
1277-
console.debug("[Playback] stale refresh " + JSON.stringify({
1278-
staleTopicCount: staleTopics.length,
1279-
messageCount: messages.length,
1280-
freshCount: freshMessages.length,
1281-
currentTime: this._currentTime,
1282-
}));
1262+
this._staleRefreshInFlight = true;
1263+
void (async () => {
1264+
try {
1265+
const messages = await this._source.getBackfillMessages({
1266+
time: referenceTime,
1267+
topics: staleTopics,
1268+
});
1269+
if (!this._isPlaybackEpochCurrent(epoch)) {
1270+
return;
1271+
}
1272+
// Same reasoning as in _handleEmptyBatch: latched topics would otherwise
1273+
// get re-delivered on every refresh tick (~5 Hz), causing panels like
1274+
// the 3D/URDF renderer to rebuild from scratch and leak GPU buffers.
1275+
const freshMessages = this._filterAlreadyDeliveredMessages(messages);
1276+
if (freshMessages.length === 0) {
1277+
return;
1278+
}
1279+
this._distributeMessages(freshMessages, referenceTime);
1280+
if (this._debugEnabled) {
1281+
console.debug("[Playback] stale refresh " + JSON.stringify({
1282+
staleTopicCount: staleTopics.length,
1283+
messageCount: messages.length,
1284+
freshCount: freshMessages.length,
1285+
currentTime: this._currentTime,
1286+
}));
1287+
}
1288+
} catch (err) {
1289+
console.warn("IterablePlayer: stale topic refresh failed", err);
1290+
} finally {
1291+
this._staleRefreshInFlight = false;
12831292
}
1284-
} catch (err) {
1285-
console.warn("IterablePlayer: stale topic refresh failed", err);
1293+
})();
1294+
}
1295+
1296+
private _staleThresholdNsForTopic(topic: string): bigint {
1297+
const stats = this._initialization?.topicStats[topic];
1298+
const frequency = stats?.frequency;
1299+
if (typeof frequency !== "number" || !Number.isFinite(frequency) || frequency <= 0) {
1300+
return BACKFILL_STALE_THRESHOLD_NS;
12861301
}
1302+
const topicPeriodNs = BigInt(Math.ceil(1_000_000_000 / frequency));
1303+
const topicThresholdNs = topicPeriodNs * BigInt(BACKFILL_STALE_TOPIC_PERIOD_MULTIPLIER);
1304+
return topicThresholdNs > BACKFILL_STALE_THRESHOLD_NS ? topicThresholdNs : BACKFILL_STALE_THRESHOLD_NS;
12871305
}
12881306

12891307
/**

src/features/viewer/RosViewProvider.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import React, { createContext, useContext, useEffect, useMemo, useState } from '
22
import { IntlProvider } from 'react-intl';
33
import { getRosViewMessages, type RosViewLocale } from '@/shared/intl/loadRosViewMessages';
44
import { Toaster } from '@/shared/ui/sonner';
5+
import { useMessagePipeline } from '@/core/pipeline/useMessagePipeline';
6+
import type { MessagePipelineState } from '@/core/pipeline/store';
57

68
export interface RosViewProviderProps {
79
theme?: 'light' | 'dark' | 'system';
@@ -73,13 +75,17 @@ export const RosViewProvider: React.FC<RosViewProviderProps> = ({
7375
);
7476

7577
const messages = useMemo(() => getRosViewMessages(language), [language]);
78+
const playerPresence = useMessagePipeline(
79+
(state: MessagePipelineState) => state.playerState.presence,
80+
);
7681

7782
return (
7883
<RosViewThemeContext.Provider value={contextValue}>
7984
<div
8085
id="rosview-root"
8186
data-language={language}
8287
data-theme={resolvedTheme}
88+
data-player-presence={playerPresence}
8389
className={`w-full h-full ${resolvedTheme === 'dark' ? 'dark' : ''}`}
8490
>
8591
<IntlProvider locale={intlLocaleFor(language)} defaultLocale="en" messages={messages}>

0 commit comments

Comments
 (0)