player-internals
GitHub详解IterablePlayer状态机、Tick循环及数据源迭代架构。涵盖状态转换逻辑、带预算控制的播放循环、防抖状态发射机制,以及序列化/反序列化数据源的包装与缓存层级结构。
触发场景
安装
npx skills add lichtblick-suite/lichtblick --skill player-internals -g -y
SKILL.md
Frontmatter
{
"name": "player-internals",
"description": "Deep implementation details of the IterablePlayer state machine, tick loop, and data source iteration patterns."
}
Player Internals Skill
State Machine Detail
preinit ──► initialize ──► start-play ──► idle
│ ▲
▼ │
play
│
▼
seek-backfill ──► idle
idle/play ──► reset-playback-iterator ──► idle/play (re-enters)
any ──► close
State Transitions
preinit → initialize: triggered the first time playback starts after construction (source supplied via constructor).initialize → start-play: Sourceinitialize()resolved, topics/schemas availablestart-play → idle: Initial backfill complete, first state emittedidle → play: User presses play orsetPlaybackSpeed(speed > 0)play → idle: Reached end of data or user pausesplay → seek-backfill: User seeks during playbackidle → seek-backfill: User seeks while pausedseek-backfill → idle: Backfill messages found, state emitted
Tick Loop Implementation
// Simplified tick loop logic
async #statePlay() {
const tickStart = performance.now();
const budgetMs = 300; // Max time per tick before yielding to UI
while (performance.now() - tickStart < budgetMs) {
const result = await this.#iterator.next();
if (result.done) { return "idle"; }
this.#pendingMessages.push(result.value.msgEvent);
// Check if we've passed the target wall-clock time
if (this.#hasReachedPlaybackTarget()) { break; }
}
this.#emitState();
return "play"; // continue playing next tick
}
Debounced State Emission
#emitStateImpl()is scheduled viaqueueMicrotaskto coalesce rapid updates- State includes:
activeData(messages, currentTime, topics),progress(caching status) - Only emits if state actually changed (reference equality check on key fields)
Iterator Architecture
There is no concrete DataSource type in this layering. Sources implement one of two interfaces:
ISerializedIterableSource (yields raw bytes) or IDeserializedIterableSource (yields decoded
MessageEvents). A serialized source must be wrapped by DeserializingIterableSource; an
already-deserialized source skips that wrapper.
Concrete source (e.g. McapIndexedIterableSource, RemoteFileReadable-backed, WebSocket, …)
│ implements ISerializedIterableSource OR IDeserializedIterableSource
▼
DeserializingIterableSource (ONLY for serialized sources — applies parseChannel-based decode)
│ packages/suite-base/src/players/IterablePlayer/DeserializingIterableSource.ts
▼
CachingIterableSource (LRU block cache, ~600MB budget)
│
▼
BufferedIterableSource (producer-consumer, read-ahead, default { sec: 10 })
│
▼
IterablePlayer (tick loop consumes messages)
⚠️
DeserializingIterableSourceis optional — it is only inserted when the underlying source is serialized (ISerializedIterableSource). Sources that already returnIDeserializedIterableSourcebypass it.
Backfill Strategy
When seeking to time T:
- For each subscribed topic, find the last message at or before T
- Uses reverse iteration in indexed sources (MCAP) for efficiency
- These messages become the "latched" state — panels see them immediately
- Critical for panels that display "latest value" (e.g., 3D transforms, image)
Subscription Management
- Subscriptions are set by panels via
MessagePipeline.setSubscriptions() - Player diffs new vs old subscriptions to avoid unnecessary re-iteration
- Topic preloading is separate from active subscriptions (handled by BlockLoader)
reset-playback-iteratorstate: when subscriptions change mid-play, iterator must restart from current time
Performance Critical Paths
- Tick loop budget: 300ms cap prevents UI freeze during catch-up
- Message accumulation: Messages are batched per tick, not emitted individually
- Iterator yielding:
awaitin the loop allows microtask scheduling - Worker sources: Heavy parsing happens in
WorkerIterableSourceoff main thread - Seek optimization: Indexed MCAP enables O(log n) seek via chunk indexes
版本历史
- cab9317 当前 2026-07-24 12:17


