Skip to content

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.

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 machine
stream_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.

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.

stream.py
from pathlib import Path
from tatolab.stream import CameraSource, H264Encoder, Mp4Sink, StreamBuilder, stream
RECORDING = Path(__file__).resolve().parent / "shared-camera.mp4"
@stream
def 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"))
@stream
def 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:

Terminal window
tatolab run -d stream.py:camera_hub # kept: runs until you stop it, and after restarts
tatolab run stream.py:hub_recorder # attached: records until you press Ctrl-C

A 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 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_remote in tatolab graph with 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.

An address names one output: <stream>/<node>/<port>, as in camera_hub/camera/video.

  • Node names default to the class name in lowercase (camerasource). Pass name= to stream_builder.add for an address that reads well, as camera_hub does with name="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 becomes camera-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 .machine prints it. An address naming another machine is refused for now.

tatolab tap and tatolab exchange take the same addresses.

Terminal window
tatolab expose camera_hub camera video # make it private
tatolab expose camera_hub camera video --public # make it public
tatolab expose camera_hub camera video --remove # back to internal

The 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.