Skip to content

Live Streaming

Instead of saving a scene and viewing it afterwards, LiveViewer starts the server immediately and pushes each state to an already-open browser tab as your simulation produces it, over a WebSocket:

from simview import LiveViewer

# `scene` needs its complete model (terrain, bodies, ...) up front; states are
# streamed in afterwards.
with LiveViewer(scene, open_browser=True) as live:
    for t in range(num_steps):
        ...  # step the simulation
        live.push_state(time=t * dt, body_states=[...], scalar_values=...)

# scene.states was appended to exactly like scene.add_state would, so it can
# still be saved once streaming is done:
scene.save("recording.json.gz", compress=True)

push_state has the same signature and validation as SimulationScene.add_state -- it delegates to it directly, then hands the new frame to a background sender thread that broadcasts it to every connected viewer. A viewer opened after the stream has already started still gets the history so far (the most recent 10,000 frames on a longer run), replayed as a catch-up before it starts receiving new frames live. Every WebSocket {"states": [...]} message also carries "frameOffset", the absolute index (into the full run, the same index space model.episodes[].startIndex uses) of its first frame, so a viewer that only received the buffered tail still places episode boundaries correctly. If no viewer is connected yet, pushed frames are simply buffered for the next one to connect. Playback in the browser follows the live frames automatically as long as you haven't scrubbed backward or started a loop; a small "LIVE" badge shows while the socket is open.

The live stream's WebSocket only accepts the server's own pages and localhost connections: a viewer opened from another machine must reach the server by its address (e.g. http://10.0.0.5:5420), not by its hostname -- a hostname-served page is indistinguishable from a page on someone else's site, so it is refused (the browser shows an error instead of the loading splash, and the server logs the rejection).

Backpressure

push_state never blocks your simulation loop on the network. Frames go into a bounded queue (queue_size, 256 by default) drained by the sender thread, so a slow or hung browser tab costs the caller nothing. When that queue fills up, the oldest pending frame is dropped to make room for the newest, keeping what the viewer shows close to live instead of falling further behind:

with LiveViewer(scene, queue_size=64) as live:
    ...
print(live.dropped_frames)  # frames skipped on the wire, if any

Dropping only affects the live stream. Every pushed frame still lands in scene.states and in the catch-up buffer, so the saved recording is complete either way. stop() (and therefore the with block's exit) flushes whatever is still queued before shutting the server down.

See example_live.py for a runnable end-to-end example.

See the LiveViewer API reference for the full signature.