Some visualizations animate, pulse, and flash. If you're sensitive to motion or flashing, turn on Reduce motion in your system settings.

poopdeck.gl
Playback

SttPlayer

SttPlayer is the HTMLMediaElement-shaped facade over the TimeController and the PlaybackGovernorthe recommended single entry point for playback. Underneath sit a dumb rAF clock, a buffering state machine, and the BufferSource oracle; the facade owns the choreography of coordinating them:

  • One driver. Play/pause/seek/scrub all route through the governor's gates; speed routes through the controller — and you never have to remember which is which (or that calling controller.play()/setTime() directly bypasses the gates).
  • The baseRate × playbackRate speed model. Video's literal 1× is meaningless for sim time; the data-player convention is a per-dataset base rate ("the dataset plays in its target duration") with a user-facing multiplier on top. The facade owns that split.
  • A throttled timeupdate. Media elements fire timeupdate at ~4 Hz; the facade does the same (configurable), with an immediate emit whenever the clock freezes or a seek lands — so UIs never rest on a stale frame, and React consumers don't re-render at 60 Hz. The internal clock still advances every animation frame; layers read it directly.

The wrapped pieces stay exposed (player.timeController for layer wiring, player.governor for advanced use), so nothing the lower-level APIs can do is lost.

Installation#

import { SttPlayer } from '@poopdeck.gl/playback';

Quick start#

const player = new SttPlayer({
timeRange: { start, end },
baseRate: (end - start) / 60_000, // dataset plays in ~60 s at 1×
loop: true,
});
new AnimatedTripsLayer({
data: manifestUrl,
// Layers READ the clock; the player drives it.
timeController: player.timeController,
// The tileset is the governor's BufferSource — hand it over when it arrives:
onTilesetReady: (tileset) => player.setSource(tileset),
// Buffer events re-evaluate gates immediately (vs the 250 ms poll):
onBufferChange: (runway) => player.notifyBufferChange(runway),
});
playButton.onclick = () => (player.paused ? player.play() : player.pause());
// Scrubbing: preview is free, release commits a seek.
slider.onpointerdown = () => player.beginScrub();
slider.oninput = (e) => player.scrubTo(+e.target.value);
slider.onpointerup = (e) => player.endScrub(+e.target.value);
// UI feedback — media-element shaped:
player.on('timeupdate', (t) => updateSlider(t)); // ~4 Hz + final snap
player.on('play', () => setGlyph('pause'));
player.on('pause', () => setGlyph('play'));
player.on('waiting', ({ etaMs }) => showSpinner(etaMs));
player.on('ready', () => hideSpinner());
player.on('ended', () => showReplayAffordance());

HTMLMediaElement mapping#

HTMLMediaElementSttPlayerNotes
play() / pause()play() / pause()Routed through the governor's buffered-runway gates. play() at the ended boundary replays from the range start (media replay convention).
pausedpausedUser intent: stays false through starting/buffering/seeking gates, so the play/pause glyph follows one bit.
endedended + 'ended' eventTrue while parked at a non-looping range boundary (distinct from a user pause).
currentTime get/setcurrentTime get/setSetter = committed seek (prefetch flush + post-seek gate). For drags use the scrub trio — previews are free.
durationdurationRange span in sim-ms. Times are absolute sim-ms (not zero-based seconds); seekable says where the range lives.
playbackRateplaybackRateThe multiplier over baseRate. Magnitude-only, like the media element — direction is separate (timeController.setDirection).
seekableseekableThe configured time range; replace via setTimeRange(range).
bufferedbufferedgovernor.getBufferedRanges() passthrough, for a buffered-bar UI.
readyStatestateThe governor machine state (idle/starting/playing/buffering/seeking).
'timeupdate' (~4 Hz)'timeupdate'Throttled to timeupdateHz; emits immediately on pause/seek/scrub so UIs land on the final value.
'waiting' / 'canplay''waiting' / 'ready'Gate entered (clock frozen) / gate passed (degraded: true when the escape hatch fired).
'progress''progress'Forwarded buffer-runway events.
'ratechange''ratechange'Fires on playbackRate changes only — baseRate re-bases what "1×" means without firing it.

Constructor#

new SttPlayer(options: SttPlayerOptions)
OptionTypeDefaultDescription
timeRange{ start, end }requiredThe dataset's time range (absolute sim-ms). Drives duration/seekable and the clock's boundary behavior.
initialTimenumbertimeRange.startInitial playhead position.
baseRatenumber1Sim-ms per wall-ms at playbackRate 1 — the per-dataset "1×".
playbackRatenumber1User-facing speed multiplier.
loopbooleanfalseWrap to the range start at the end. Wraps are routed through seek semantics by the governor (see loop wraps).
bouncebooleanfalsePing-pong at the boundaries instead of wrapping (see TimeControllerOptions.bounce).
timeupdateHznumber4'timeupdate' cadence. Throttles ONLY the event — never the internal clock.
governorPlaybackGovernorOptions minus source{}Gate/watermark/escape-hatch tuning. The source is wired via setSource().

Methods and properties#

Playback#

Everything in the mapping table above, plus:

MemberDescription
isCreepingDegraded-creep flag: playing, pinned to the buffered frontier at data-arrival rate.
baseRate get/setThe per-dataset "1×"; setter re-applies the effective speed without firing 'ratechange'.
setTimeRange(range)Replace the range (a dataset switch) — re-bases duration and seekable.

Scrubbing#

beginScrub() / scrubTo(time) / endScrub(time) — preview-vs-commit passthroughs to the governor (grab freezes the clock; previews render resident tiles with no fetch churn; release commits a real seek). The readonly seekSettleMs field is the shared settle-debounce knob for UIs that commit a rested thumb mid-drag. The isScrubbing getter is true while the thumb is held (beginScrubendScrub) — a passthrough to governor.isScrubbing.

Plumbing and queries#

MemberDescription
setSource(source)Attach the readiness oracle (the tileset, from the layer's onTilesetReady).
notifyBufferChange(runway)Forward the layer's onBufferChange; re-emitted as 'progress', re-evaluates gates immediately.
getEtaMs()Honest wall-ms ETA for the current gate window; null when unknown.
estimateCost(range)Byte/tile cost of buffering range (directory math — ETA chips, timeline density strips).
getQoeStats()Session QoE counters (stalls, startup, creep, seeks + settle p50, gate entries by reason, frontier snap-backs, permanent blocks) — see QoE counters.
getAutoSpeedSuggestion()Raw sustainable speed in controller units (sim-ms per wall-ms).
getAutoSpeedMultiplierSuggestion()The same suggestion ÷ baseRate — directly comparable to playbackRate. May be Infinity (fully-buffered horizon ⇒ no network cap): clamp/snap/damp via decideAutoSpeedMultiplier, never apply it raw.
timeController / governorThe wrapped pieces, for layer wiring and advanced use.
destroy()Dispose the governor, destroy the clock, drop all listeners. Idempotent.

Events#

const unsubscribe = player.on('timeupdate', (time) => {});
player.on('play', () => {}); // intent became "playing" (incl. adopted external play)
player.on('pause', () => {}); // intent became "paused" (incl. external pause / range-end clamp)
player.on('statechange', (state) => {});
player.on('waiting', ({ state, etaMs }) => {});
player.on('ready', ({ degraded }) => {});
player.on('progress', (runway) => {});
player.on('ended', (time) => {});
player.on('ratechange', (rate) => {});
player.on('scrubstart', (time) => {}); // scrubber grabbed (payload: playhead at the grab)
player.on('scrubend', (time) => {}); // scrubber released (payload: committed position)

on() returns an unsubscribe function; off(event, callback) also works. At a non-looping range end, 'pause' fires before 'ended' (media-element ordering). 'scrubstart' / 'scrubend' re-emit the governor's scrub bracket (see PlaybackGovernor events).

Speed model#

effective clock speed (sim-ms per wall-ms) = baseRate × playbackRate

Pick baseRate so the dataset plays in a target wall duration (span / targetMs); expose playbackRate to the user as the 0.25×–10× control. For Auto speed, feed getAutoSpeedMultiplierSuggestion() through decideAutoSpeedMultiplier on a cadence + on 'waiting' — see the governor docs for the asymmetric policy.

Source#

packages/playback/src/stt-player.ts · underlying pieces: TimeController, PlaybackGovernor