Share between streams
Expose a stream's outputs so other streams on the machine can read them, control who can, and change that while everything runs.
A stream doesn’t have to do everything itself. Expose an output, and any other stream on the machine can read it by its address. The stream that produces it doesn’t change, and doesn’t need to know who reads it. Open a camera once, and let a recorder, a detector and a dashboard each read it from streams of their own, which you start, stop and change independently.
Expose an output
Section titled “Expose an output”A stream’s outputs are internal until it exposes them:
from tatolab.stream import Exposure
stream_builder.expose(camera.output("video")) # other streams on this machinestream_builder.expose(camera.output("video"), Exposure.PUBLIC) # and other machines| Level | Who can link to the output |
|---|---|
| internal (the default) | Only nodes in the same stream |
private (expose(output)) |
Other streams on this machine |
public (Exposure.PUBLIC) |
Readers on other machines too, by URL. See Across machines. |
Inside a stream, exposure never matters: any node can connect to any port. It controls only links that cross the stream’s edge.
Looking is different from linking. tatolab tap, tatolab exchange and an agent’s sampling tools can read any output that something reads, exposed or not, because they’re for inspecting your own streams. An output exposes its stream’s interface; inspection sees everything.
Read another stream’s output
Section titled “Read another stream’s output”stream_builder.remote_output names another stream’s output by its address, <stream>/<node>/<port>, and you connect it like one of your own. Here’s one project with two streams: one opens the camera and shares it, the other records it.
from pathlib import Path
from tatolab.stream import CameraSource, H264Encoder, Mp4Sink, StreamBuilder, stream
RECORDING = Path(__file__).resolve().parent / "shared-camera.mp4"
@streamdef camera_hub(stream_builder: StreamBuilder) -> None: """Opens the camera once and shares it with other streams on this machine.""" camera = stream_builder.add(CameraSource, name="camera") stream_builder.expose(camera.output("video"))
@streamdef hub_recorder(stream_builder: StreamBuilder) -> None: """Records the camera that camera_hub shares.""" encoder = stream_builder.add(H264Encoder) recorder = stream_builder.add(Mp4Sink, config={"path": str(RECORDING)}) stream_builder.connect(stream_builder.remote_output("camera_hub/camera/video"), encoder.input("video")) stream_builder.connect(encoder.output("encoded_video"), recorder.input("tracks"))Keep the camera running, and record when you want to:
tatolab run -d stream.py:camera_hub # kept: runs until you stop it, and after restartstatolab run stream.py:hub_recorder # attached: records until you press Ctrl-CA third stream, say a detector, can read camera_hub/camera/video too, and the camera is still opened only once. The streams can just as well live in different projects, with different dependencies, so one team’s model can read the camera another team’s stream exposes.
Running the recorder attached is deliberate. Mp4Sink starts its file over each time its stream loads, and a kept stream loads again after every restart, so a kept recorder would overwrite its last recording after a reboot. See Record and encode.
The rules
Section titled “The rules”- The reading stream owns the link. A stream pulls from another’s output; no stream can push into another’s input.
- Order doesn’t matter. If the reader loads first, its link waits, shown as
awaiting_remoteintatolab graphwith the reason, and connects when the source stream starts. - The output must be exposed. A link to an internal output waits too, naming the port and its level, until the output is exposed. An agent connecting live to an internal output is refused, with the same explanation.
- Frames aren’t copied. Streams on one machine share frames in GPU memory, just as nodes in one stream do.
- Readers can’t slow the source. Each reader reads at its own pace with its own delivery profile. One that falls far behind drops bags at its own input, counted in its metrics, and the source never waits.
Addresses
Section titled “Addresses”An address names one output: <stream>/<node>/<port>, as in camera_hub/camera/video.
- Node names default to the class name in lowercase (
camerasource). Passname=tostream_builder.addfor an address that reads well, ascamera_hubdoes withname="camera". - Every part is made URL-safe: lowercased, accents dropped, and anything other than letters, digits,
-,.,_and~turned into-. You can write"Camera Hub"and it becomescamera-hub. - The full form starts with the machine’s name:
desk/camera_hub/camera/video. The machine’s name is its Tailscale machine name, or its hostname when it isn’t on a tailnet;tatolab graph | jq -r .machineprints it. An address naming another machine is refused for now.
tatolab tap and tatolab exchange take the same addresses.
Change exposure while it runs
Section titled “Change exposure while it runs”tatolab expose camera_hub camera video # make it privatetatolab expose camera_hub camera video --public # make it publictatolab expose camera_hub camera video --remove # back to internalThe change takes effect at once, with nothing restarted. Lowering an output’s level immediately cuts off every reader the new level doesn’t allow: their links are removed, and the reading streams keep running without them. For a kept stream, the runtime records the level you set, and it takes precedence over what the stream function exposes, across restarts.