Push a frame instead of storing and fetching it
The rate the media dtypes could carry was one frame every second or two: each
was a file on the data volume, an event on the socket, and a request back for
the bytes. This closes both halves of that, and they are one feature.
`save_artifact(..., volatile=True)` writes to a `VolatileStore` — the same
content-addressed store, in `/dev/shm`, bounded by size with the oldest falling
out (`ARTIFACT_VOLATILE_BYTES`, 48 MB under the container's raised `shm_size`).
Nothing sweeps it: a frame nobody kept is not worth walking the store to find.
`ArtifactStore.path` falls through to it, which is what lets a volatile frame be
an ordinary reference everywhere else — the dtype check, a panel's digest scope,
`load_artifact` in a node, and the widget's own fetch all work on one unchanged.
`adopt` copies one into the store when a run records it, so "returned media is
kept, emitted media is not" stays true.
The bytes then go down the flows websocket as a length-prefixed binary frame,
sent just ahead of the `message_value` naming them, so a tile has the frame when
it hears the value moved. Nothing is pushed unasked: a client names the messages
it is drawing (`{"type":"media","names":[…]}`), a panel's list is intersected
with the scope it already had, and only the newest frame per name in a batch is
sent — a client that fell behind is not handed frames it would draw over. The
tunnel relays text only, so a screen reached through a portal falls back to
fetching, which is why the rate table now has two rows.
Around the edges: the remote worker's fetch cache is bounded at last
(`FLUKSIO_ARTIFACT_CACHE_BYTES`), since content addressing means nothing in it
ever expires and a media stream fills it with chunks nothing asks for twice; a
port carrying an image draws the frame in the node panel rather than only
saying `image/png · frame.png · 1.79kB`; and an edge chip says that much instead
of a line of hash. The media screenshot stops waiting for `networkidle` — a
camera is a socket that never goes quiet, which is the point of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YC4u66vjzW54fnHu5Juhh9
This commit is contained in:
@@ -177,7 +177,9 @@ connector publishes a reference to it instead:
|
||||
async def poll(self) -> dict[str, Any] | None:
|
||||
jpeg = await asyncio.to_thread(self._grab)
|
||||
return {
|
||||
"frame": self.save_artifact(jpeg, "frame.jpg", media_type="image/jpeg")
|
||||
"frame": self.save_artifact(
|
||||
jpeg, "frame.jpg", media_type="image/jpeg", volatile=True
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -186,10 +188,18 @@ async def poll(self) -> dict[str, Any] | None:
|
||||
works once the node has started, since the store is the engine's and is handed
|
||||
over then.
|
||||
|
||||
`volatile=True` is what a camera publishes with. The frame goes to a ring in
|
||||
memory rather than the data volume and is pushed down the websocket to
|
||||
whichever screens are drawing it, so a wall panel sees ten frames a second and
|
||||
the SD card under it is never written to. Frames last until newer ones need the
|
||||
room; leave the flag off for a reading somebody asked to keep.
|
||||
|
||||
Each reading is a new artifact, which the poll loop publishes because its
|
||||
digest differs from the last. Set `poll_interval` to what somebody actually
|
||||
wants to look at: a frame every second or two is a glance, and live video
|
||||
belongs on the camera's own stream rather than in the graph.
|
||||
wants to look at. Through a portal the bytes are fetched rather than pushed, so
|
||||
a remote panel wants a frame every second or two; higher rates than that are
|
||||
for the local network, and full-rate video still belongs on the camera's own
|
||||
stream.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
|
||||
Reference in New Issue
Block a user