---
title: "Share between streams"
description: "Expose a stream's outputs so other streams on the machine can read them, control who can, and change that while everything runs."
type: "docs-page"
url: "https://tatolab.com/docs/share/between-streams"
site: "Tatolab"
section: "Share"
---

# 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

A stream's outputs are internal until it exposes them:

```python
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](https://tatolab.com/docs/share/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

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

```python title="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:

```sh
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](https://tatolab.com/docs/build/recording#recordings-and-restarts).

## 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_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.

## 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`). 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.

## Change exposure while it runs

```sh
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.
