Metadata-Version: 2.5
Name: pyst2110
Version: 0.9.1
Summary: SMPTE ST 2110 protocol: RTP and RFC 4175 headers, geometry, SDP
Project-URL: Repository, https://github.com/Fuse-Technical-Group/pyst2110
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Video
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: numpy>=2
Description-Content-Type: text/markdown

# pyst2110

SMPTE ST 2110 protocol for Python: RTP and RFC 4175 headers read and
written, frame boundaries, sequence-loss accounting, pgroup geometry, and
SDP.

> [REQUIREMENTS.md](REQUIREMENTS.md) — the problem.
> [SPEC.md](SPEC.md) — the design and its rationale.
> [ROADMAP.md](ROADMAP.md) — what is not built yet.

| Layer | Owns |
| --- | --- |
| A transport binding (a NIC binding, a socket, a capture file) | Moving bytes |
| **pyst2110** | RTP + RFC 4175 headers, geometry, SDP, ST 2110-21 timing |
| The consuming runtime | Pixels — packing, unpacking, colour |

The layer between a transport and a raster: it says what packet headers
mean and where their payloads belong, and never moves a pixel itself.
numpy is the only runtime dependency — no vendor SDK, no transport
binding, no NIC and no licence, so it runs anywhere CI does.

## Install

A git dependency pinned by tag. Publication to PyPI waits for a consumer
depending on a released version (§road:pypi):

```bash
uv add "pyst2110 @ git+https://github.com/Fuse-Technical-Group/pyst2110@v0.2.0"
```

## Usage

```python
from pyst2110 import parse_sdp, parse_video_format

offer = open("flow.sdp").read()
flow = parse_sdp(offer)          # destination address, port, source filter
video = parse_video_format(offer)  # width, height, rate, sampling, depth
```

Parsing is vectorized over whole chunks: every function takes a
two-dimensional `uint8` array, one row per packet, and returns one array
per field — the shape a header-data-split receiver already hands out.

Header fields are reported as the wire declared them, this being a parse
and not a filter. A packet is free to name a row outside the image, so
`fits_raster` masks the descriptors that name a place inside the flow's
raster and a consumer places only those:

```python
fits = fits_raster(video, payload.line, payload.offset_samples)
starts = raster_offset(video, payload.line[fits], payload.offset_samples[fits])
```

Sending is the same shape in reverse. A frame's headers are built once
for a format and payload size, then stamped per frame with the only two
fields that move — the sequence numbers and the media timestamp:

```python
from pyst2110 import FrameHeaders, choose_payload_size, format_sdp, max_payload_size

payload_size = choose_payload_size(video, max_payload_size(video))
frame = FrameHeaders(video, payload_size, ssrc=0x1234ABCD)
for index in range(frames):
    headers = frame.stamp(index)   # (packets, 20) uint8, one row per packet
    ...                            # send each header with frame.frame_offset_octets
offer = format_sdp(flow, video, session_name="my sender")  # what was sent
```

The offer carries the media type parameters ST 2110-20 section 7.2 and
ST 2110-21 section 8.1 require of a sender. `TP` describes the pacing,
which is the transport's and not this library's, so `sender_type=` is
the caller's to set — it defaults to `2110TPN`. A multicast `flow` names
its sender or passes `any_source=True`; see §spec:sdp for why that is a
choice rather than a default.

An ST 2022-7 redundant pair is one offer and not two. `format_dup_sdp`
writes RFC 7104's grouping — a session-level `a=group:DUP` over two
`m=video` blocks, each with its own address and sender — and
`parse_dup_sdp` reads both legs back, in the order the group names them:

```python
from pyst2110 import format_dup_sdp, parse_dup_sdp

offer = format_dup_sdp(red, blue, video, session_name="my sender")
first, second = parse_dup_sdp(offer)
```

A document whose `DUP` tags and media blocks disagree is refused rather
than read as a single-leg offer, and `parse_sdp` refuses a grouped offer
for the same reason: a sender that emitted one leg where two were meant
sends unprotected essence and reports success.

`session_name` defaults to the single space RFC 4566 section 5.3 prescribes
for a session with no meaningful name — and at least one transmit SDK refuses
it, so name the session where a sender will read the offer back. NVIDIA
Rivermax reports `'x=<token>' format not found` and fails stream creation,
accepting the same document once it is named.

`frame_offset_octets` says which octets of the frame buffer each packet
carries. Moving them is the consumer's, as on the receive side.

`stamp` hands back the same array every time, restamped in place — that
is what keeps the loop above from allocating a frame of headers per
frame. So `headers` is only valid until the next `stamp`: a caller
queueing two frames at once copies the first.

## API

Everything is re-exported from the top-level package, and the
docstrings there are the authority; `help(pyst2110)` is the index.

Both paths are built: SDP parsing and emit — the single-leg offer and the
ST 2022-7 pair — the RFC 3550 header parse, format geometry, sequence and
frame tracking, RFC 4175 payload descriptors, the transmit header block,
the ST 2022-7 reconstruction of one flow from two legs, and the ST 2110-21
timing model — read schedules, sender limits, and the two leaky buckets
that judge a capture's emission instants. What is not built is listed in
[ROADMAP.md](ROADMAP.md).

## Development

```bash
uv sync
bash tools/ci.sh
```

`tools/ci.sh` is the gate CI runs — ruff, mypy, pytest.

## License

MIT — see [LICENSE](LICENSE). The standards this implements are public and
an implementation of them should be too.
