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.