Skip to content

Commit 484b126

Browse files
committed
fix(playback): replace best-effort max with 10x
Use a deterministic upper playback rate so visualization load and timing remain predictable across devices.
1 parent c0958d8 commit 484b126

9 files changed

Lines changed: 32 additions & 40 deletions

File tree

‎src/core/players/IterablePlayer.test.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,25 @@ afterEach(() => {
120120
messageBus.reset();
121121
});
122122

123+
describe('IterablePlayer playback speed', () => {
124+
it('uses a deterministic 10x maximum instead of a best-effort sentinel', async () => {
125+
const player = new IterablePlayer(makeSource([]));
126+
let latestState: PlayerState | undefined;
127+
player.setListener((state) => {
128+
latestState = state;
129+
});
130+
await player.initialize({});
131+
132+
player.setSpeed(10);
133+
expect(latestState?.activeData?.speed).toBe(10);
134+
135+
player.setSpeed(64);
136+
expect(latestState?.activeData?.speed).toBe(10);
137+
138+
player.close();
139+
});
140+
});
141+
123142
describe('IterablePlayer high-frequency lane', () => {
124143
it('routes video-only topics outside the generic message bus', async () => {
125144
const source = makeSource([makeImageMessage()]);

‎src/core/players/IterablePlayer.ts‎

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {
66
StreamMessagesInTimeRangeArgs,
77
Subscription,
88
} from '@/core/types/player';
9-
import { PLAYBACK_SPEED_MAX } from '@/core/types/player';
9+
import { MAX_PLAYBACK_SPEED } from '@/core/types/player';
1010
import type { DataQualityReport, Time, Initialization, MessageEvent, TimeRange } from '@/core/types/ros';
1111
import type { ISourceHandle } from '@/infra/workers/ISourceHandle';
1212
import type { IMessageCursor } from '@/infra/workers/types';
@@ -469,7 +469,7 @@ export class IterablePlayer implements Player {
469469
this._advancePlaybackEpoch();
470470
this._isPlaying = true;
471471
const now = performance.now();
472-
this._clock.play(this._currentTime, this._speedFactor(), now);
472+
this._clock.play(this._currentTime, this._speed, now);
473473
this._lastTickWallMs = now;
474474
this._pageSuspended = typeof document !== "undefined" && document.hidden;
475475
if (this._pageSuspended) {
@@ -600,19 +600,11 @@ export class IterablePlayer implements Player {
600600
}
601601

602602
setSpeed(speed: number): void {
603-
if (speed === PLAYBACK_SPEED_MAX) {
604-
this._speed = PLAYBACK_SPEED_MAX;
605-
} else {
606-
this._speed = Math.min(8, Math.max(0.1, speed));
607-
}
608-
this._clock.setSpeed(this._speedFactor(), performance.now());
603+
this._speed = Math.min(MAX_PLAYBACK_SPEED, Math.max(0.1, speed));
604+
this._clock.setSpeed(this._speed, performance.now());
609605
this._emitState();
610606
}
611607

612-
private _speedFactor(): number {
613-
return this._speed === PLAYBACK_SPEED_MAX ? 64 : this._speed;
614-
}
615-
616608
setSamplingFps(fps: number): void {
617609
const clamped = Math.max(1, Math.min(MAX_SAMPLING_FPS, Math.round(fps)));
618610
this._samplingFps = clamped;

‎src/core/types/player.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ export interface HighFrequencyConsumer {
9797
onMessageBatch?: (messages: MessageEvent[]) => void;
9898
}
9999

100-
/** Sentinel playback speed for “as fast as possible” (see IterablePlayer / PlaybackBar). */
101-
export const PLAYBACK_SPEED_MAX = -1;
100+
/** Highest deterministic playback rate exposed by the visualization player. */
101+
export const MAX_PLAYBACK_SPEED = 10;
102102

103103
export interface Player {
104104
setListener(listener: (state: PlayerState) => void): void;

‎src/features/panels/Audio/AudioPanel.tsx‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { useIntl } from 'react-intl';
33
import { useShallow } from 'zustand/react/shallow';
44
import { timeToNs } from '@/core/analysis/timeSeries';
55
import type { Player } from '@/core/types/player';
6-
import { PLAYBACK_SPEED_MAX } from '@/core/types/player';
76
import { messageBus } from '@/core/pipeline/messageBus';
87
import { useSubscriberSeq } from '@/core/pipeline/useMessageBus';
98
import { useMessagePipeline } from '@/core/pipeline/useMessagePipeline';
@@ -100,7 +99,6 @@ export const AudioPanel: React.FC<AudioPanelProps> = (props) => {
10099

101100
const allowPlayback = useMemo(() => {
102101
if (!isPlaying || config.mute) return false;
103-
if (speed === PLAYBACK_SPEED_MAX) return false;
104102
return Math.abs(speed - 1) < 1e-4;
105103
}, [isPlaying, config.mute, speed]);
106104

@@ -259,7 +257,7 @@ export const AudioPanel: React.FC<AudioPanelProps> = (props) => {
259257
const statusLabel = useMemo(() => {
260258
if (!config.topic) return formatMessage({ id: 'panels.audio.status.waitingTopic' });
261259
if (!allowPlayback && isPlaying && !config.mute) {
262-
if (speed === PLAYBACK_SPEED_MAX || Math.abs(speed - 1) >= 1e-4) {
260+
if (Math.abs(speed - 1) >= 1e-4) {
263261
return formatMessage({ id: 'panels.audio.status.mutedNon1x' });
264262
}
265263
}

‎src/features/workspace/playback/PlaybackBar.tsx‎

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
2-
import { PLAYBACK_SPEED_MAX } from '@/core/types/player';
32
import { CalendarClock, Pause, Play, SkipBack, SkipForward, Timer } from 'lucide-react';
43
import { useShallow } from 'zustand/react/shallow';
54
import { useIntl } from 'react-intl';
@@ -53,7 +52,7 @@ function percentToTime(percent: number, start: Time, end: Time): Time {
5352
return { sec, nsec };
5453
}
5554

56-
const PRESET_SPEEDS = [0.1, 0.25, 0.5, 1, 2, 4, 8] as const;
55+
const PRESET_SPEEDS = [0.1, 0.25, 0.5, 1, 2, 4, 8, 10] as const;
5756
const PRESET_SAMPLING_FPS = [15, 30, 45] as const;
5857

5958
const MENUBAR_PLAYBACK_SPEED = 'playback-menubar-speed';
@@ -599,9 +598,7 @@ export const PlaybackBar: React.FC<PlaybackBarProps> = ({ player, extensionConte
599598
setPlaybackSettingsMenubarValue(MENUBAR_PLAYBACK_SPEED);
600599
}}
601600
>
602-
{speed === PLAYBACK_SPEED_MAX
603-
? formatMessage({ id: 'playback.speedMax' })
604-
: `${speed}x`}
601+
{speed}x
605602
</MenubarTrigger>
606603
<MenubarContent align="end" sideOffset={6}>
607604
<MenubarGroup>
@@ -616,14 +613,6 @@ export const PlaybackBar: React.FC<PlaybackBarProps> = ({ player, extensionConte
616613
{item}x
617614
</MenubarItem>
618615
))}
619-
<MenubarItem
620-
className={`text-xs ${speed === PLAYBACK_SPEED_MAX ? 'bg-accent text-accent-foreground' : ''}`}
621-
onSelect={() => {
622-
player.setSpeed(PLAYBACK_SPEED_MAX);
623-
}}
624-
>
625-
{formatMessage({ id: 'playback.speedMax' })}
626-
</MenubarItem>
627616
</MenubarGroup>
628617
</MenubarContent>
629618
</MenubarMenu>

‎src/shared/hooks/useKeyboardShortcuts.ts‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect, useRef } from 'react';
22
import type { Player } from '@/core/types/player';
3-
import { PLAYBACK_SPEED_MAX } from '@/core/types/player';
3+
import { MAX_PLAYBACK_SPEED } from '@/core/types/player';
44
import { useMessagePipeline } from '@/core/pipeline/useMessagePipeline';
55
import type { MessagePipelineState } from '@/core/pipeline/store';
66
import { resolveStepMsFromModifiers } from '@/shared/utils/playbackStep';
@@ -56,8 +56,8 @@ export function useKeyboardShortcuts(player: Player) {
5656
case 'BracketLeft': {
5757
e.preventDefault();
5858
const cur = speedRef.current;
59-
if (cur === PLAYBACK_SPEED_MAX) {
60-
player.setSpeed(4);
59+
if (cur > 8) {
60+
player.setSpeed(8);
6161
} else {
6262
player.setSpeed(Math.max(0.1, cur / 2));
6363
}
@@ -66,11 +66,8 @@ export function useKeyboardShortcuts(player: Player) {
6666
case 'BracketRight': {
6767
e.preventDefault();
6868
const cur = speedRef.current;
69-
if (cur === PLAYBACK_SPEED_MAX) {
70-
break;
71-
}
7269
if (cur >= 8) {
73-
player.setSpeed(PLAYBACK_SPEED_MAX);
70+
player.setSpeed(MAX_PLAYBACK_SPEED);
7471
} else {
7572
player.setSpeed(Math.min(8, cur * 2));
7673
}

‎src/shared/intl/messages/en/playback.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"playback.annotationsEmpty": "No annotation ranges",
3-
"playback.speedMax": "Max (best effort)",
43
"playback.play": "Play playback",
54
"playback.pause": "Pause playback",
65
"playback.timeMode.relative.aria": "Relative time from log start; click to switch to absolute local time",

‎src/shared/intl/messages/ja/playback.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"playback.annotationsEmpty": "注釈範囲がありません",
3-
"playback.speedMax": "最大(ベストエフォート)",
43
"playback.play": "再生",
54
"playback.pause": "一時停止",
65
"playback.timeMode.relative.aria": "ログ先頭からの相対時刻です。クリックで絶対時刻に切り替え",

‎src/shared/intl/messages/zh/playback.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"playback.annotationsEmpty": "暂无标注区间",
3-
"playback.speedMax": "最大(尽力加速)",
43
"playback.play": "播放",
54
"playback.pause": "暂停",
65
"playback.timeMode.relative.aria": "当前为相对时间,点击切换为绝对时间",

0 commit comments

Comments
 (0)