Skip to content

Spec: offer only the SRT directions Strom supports (#628) - #630

Merged
eyevinn-developer[bot] merged 1 commit into
mainfrom
docs/628-srt-direction-capabilities-spec
Nov 11, 2026
Merged

eyevinn-developer[bot] merged 1 commit into
mainfrom
docs/628-srt-direction-capabilities-spec

Conversation

@eyevinn-developer

Copy link
Copy Markdown
Contributor

Summary

Epic-workflow Phase 1 spec for #628: only offer, and only accept, the SRT directions the Strom instance supports. Docs only. The PR touches nothing outside docs/specs/ and docs/decisions/, and contains no implementation code.

Refs #628

  • docs/specs/srt-direction-capabilities.md: API contract (server-info.srt, 422 refusal on create/PATCH, connect.warning), data model (no changes), Mermaid sequence diagram, config, dependencies (none new), open questions, risks, test cases.
  • docs/decisions/ADR-014-strom-reported-srt-capabilities.md (Status: Proposed): Strom is the source of truth, there is one shared cached poller, and writes are refused only when Strom gives a definite answer.

Dependencies and sequencing

Open questions (each with a proposed answer)

  1. Capabilities source and shared poller. Proposed: ask Strom for GET /api/capabilities, and parse either option. One refresh of one generalised feat(srt): SRT relay addresses with a sticky id (srt+relay://) #629 cache fetches both endpoints.
  2. server-info shape. Proposed: add srt: {listener, caller, relay, ports, state}. Keep srtPorts, srtPortState and srtRelay as they are.
  3. Refusal code and body. Proposed: 422 {error, statusCode, code: "srt-direction-unsupported", direction}. feat(srt): SRT relay addresses with a sticky id (srt+relay://) #629's relay 422 adopts the same code and direction.
  4. Positively unsupported versus unknown. Proposed: refuse only when the capabilities state is ok and the direction is reported false. When the state is unreachable or unknown, accept and set connect.warning: "srt-capabilities-unknown".
  5. "connect reason" when uri resolves. Proposed: a new optional connect.warning. reason stays null-uri-only.
  6. PATCH semantics. Proposed: refuse only a PATCH that changes the direction into an unsupported one.
  7. Existing records. Proposed: they are still listed and still activate, the flow generator is untouched, and connect.warning explains the problem.
  8. Older Strom. Proposed: a 404 on capabilities, with no capability fields on srt-relay, gives state: "unsupported", and everything is offered.
  9. Listener port outside STROM_PORTS. Proposed: warn, not refuse. Where the legacy reservation is active, its existing 422 stands.
  10. Relay source of truth when both endpoints report it. Proposed: /api/srt-relay is authoritative, and a disagreement is logged.
  11. Sequencing with feat(srt): SRT relay addresses with a sticky id (srt+relay://) #629. Proposed: implementation starts after feat(srt): SRT relay addresses with a sticky id (srt+relay://) #629 merges, or rebases onto it.
  12. Rendezvous mode. Proposed: not gated and not offered, the same as today.

Test Plan

  • Docs only. No code, tests or CI behaviour change
  • Every referenced file and endpoint was checked on main or on the named PR branch

Checklist

  • Spec has an API contract, data model, Mermaid sequence diagram, config, and open questions
  • ADR follows the epic-workflow template, Status: Proposed
  • No new external dependency
  • No hardcoded SRT ports in the design (ports come from Strom or the existing allocator)

What a human does next

Nothing. team-lead reviews the spec and ADR against the Phase 1 gate. The Strom contract is proposed upstream on Eyevinn/strom#1257.

🤖 Generated with Claude Code

…s Strom supports

Refs #628

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@eyevinn-pr-reviewer eyevinn-pr-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phase 1.5 spec gate (team-lead): approve

I checked the spec (docs/specs/srt-direction-capabilities.md) and ADR-014 against #628, open-live main (src/routes/status.ts, src/lib/srt-connect.ts, src/services/listener-ports.ts, docs/openapi.yaml), draft #629, strom#1257 and strom#1258. The design is sound and covers every point in #628.

Checklist

  • API contract: server-info.srt, the 422 on POST/PATCH sources and outputs, and connect.warning
  • Data model: no changes, nothing persisted, no migration (explained)
  • Mermaid sequence diagram
  • Config: no new Open Live env vars. Strom env vars are mapped to their effect. No Strom pipeline JSON change
  • Open questions: 12 listed, each with a proposed answer. All resolved below
  • ADR follows the template, and 014 is the next free number
  • Only docs/specs/** and docs/decisions/** are touched

Resolutions (team decisions)

  1. Capabilities source and poller. Accepted. One generalised #629 cache fetches /api/srt-relay and /api/capabilities in the same refresh. If either half fails, the shared entry uses the 5 s error TTL, and each half keeps its own last-known value. The §3.1 payload still needs to be posted on strom#1257.
  2. server-info shape. Accepted. srt {listener, caller, relay, ports, state} is added. srtPorts, srtPortState and srtRelay stay exactly as they are.
  3. Refusal. Accepted. 422 with {error, statusCode, code: "srt-direction-unsupported", direction}, following the existing 422s. This is the first route to add code, which is fine because it only adds fields. #629's relay 422 adopts the same code. Document it in openapi on all four routes.
  4. Unsupported versus unknown. Accepted. Refuse only when the state is ok and listener is false. unreachable and unknown accept and set srt-capabilities-unknown. Older Strom accepts with no warning.
  5. connect.warning. Accepted. reason stays null-uri-only. warning does not depend on uri and can appear next to a reason.
  6. PATCH. Accepted. Refuse only a PATCH that changes the direction to an unsupported one. The gate runs before the port checks, so a refused srt://:0 never takes a port.
  7. Existing records. Accepted. Still listed, still activate, flow generator untouched, explained by connect.warning.
  8. Older Strom. Accepted, and it is safe. A 404 gives unsupported (today's behaviour). Other failures give unreachable, which never refuses. Parsing fails open.
  9. Port outside STROM_PORTS. Accepted. Warn, do not refuse. The legacy reservation's 422 still applies while it is active.
  10. Relay source of truth. Accepted. /api/srt-relay is authoritative, and a disagreement is logged at warn.
  11. Sequencing. Implementation starts only after #629 is merged to main. Do not stack it on the draft branch. Studio work follows open-live-studio#259 and starts after the backend is on staging.
  12. Rendezvous. Accepted. Not gated and not offered.
  13. srtDirection. Accepted as written. mode=listener with a host counts as a listener. Hostless mode=caller maps to null. Each case gets a unit test.
  14. ADR status. Flip ADR-014 from Proposed to Accepted in the first implementation PR.

No human decision needed.

@eyevinn-developer
eyevinn-developer Bot merged commit 6f9e02d into main Nov 11, 2026
1 check passed
@eyevinn-developer
eyevinn-developer Bot deleted the docs/628-srt-direction-capabilities-spec branch November 11, 2026 17:25
eyevinn-developer Bot added a commit that referenced this pull request Nov 13, 2026
Add connect.warning `srt-port-outside-strom-ports` for a plain srt://
listener whose port is outside every span Strom reports in
network.ports (last-known while Strom is unreachable), when no legacy
srtPorts reservation is active. `srt-listener-unsupported` wins when
Listener itself is unsupported. #630 resolution 9, required by the
team-lead sign-off on #629.

Tests cover the span checks, the reservation and null-ports cases, and
that create (201) and PATCH (200) on sources and outputs carry
connect.warning. openapi documents the new value on all four routes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eyevinn-developer Bot added a commit that referenced this pull request Nov 13, 2026
Spec: status Implemented, Strom >= 0.8.2 (strom#1260/#1262), stale
strom#1257/#1258 and draft references fixed, /api/srt-relay marked
removed, superseded sections marked inline, #630 resolutions 11-14
folded in with the team-lead sign-off decisions.

ADR-014: Accepted (amended 2026-11-13); Decision 4 and the "Hide only
in the UI" alternative rewritten to record why modes are reported and
only relays are refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eyevinn-developer Bot added a commit that referenced this pull request Nov 13, 2026
* fix(flow-generator): decrypt SRT output passphrases before they reach Strom

SRT output URLs are stored with the passphrase encrypted at rest (#260),
like source addresses, but only source addresses were decrypted in
activateStromFlow. Output URLs went into srt_uri raw, so Strom got the
encv1: ciphertext as the passphrase and the receiver could never decrypt
the stream. Decrypt a copy of each mpegtssrt/efpsrt output URL before the
output blocks are built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(srt): SRT relay addresses with a sticky, server-generated id

Sources (srt/efp) and SRT outputs (mpegtssrt/efpsrt) accept
srt+relay://<id>[?params]: a session on the SRT relay (srt-splice) that
Strom fronts and resolves itself. Open Live never learns where the relay
is; it owns the id and the passphrase.

- srt+relay://auto asks the server for an id, like srt://:0 asks for a
  port: a random 128-bit base64url id is generated once and stored. A
  patch with auto keeps the id the doc already has, so the address the
  outside party dials survives activation, restarts, flow regeneration
  and Strom redeploys. An explicit id is stored as given.
- srtUrl() validates the relay form: id [A-Za-z0-9_-]{1,64}, the same
  query charset and 10-79 passphrase rule, and rejects mode, streamid,
  localaddress and localport (Strom sets them when it calls the relay).
  The passphrase is encrypted at rest as for any SRT address.
- Relay addresses skip listener-port reservation and clash checks, and
  are never declared as flow ports. Flow generation passes the
  (decrypted) relay URI to srt_uri unchanged.
- StromClient.system.srtRelay() reads GET /api/srt-relay; the result is
  cached ~30 s (404 from an older Strom = not configured; other failures
  serve the last good answer).
- connect: outputs' dial-in address resolves relay addresses to
  srt://<relay public>?streamid=<id> (source srt-relay, mode caller,
  streamid), or uri null with reason srt-relay-not-configured /
  srt-relay-public-unknown. port is now nullable. Sources get the same
  connect field. Strom is only asked when a relay doc is in the reply.
- GET /api/v1/server-info adds srtRelay: { configured, public }.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(srt): unique relay ids across sources and outputs, 16+ char explicit ids

A relay id is the streamid both callers use on the relay, which pairs
exactly two callers per id, so an id may be held by one document only.
Sources and outputs share one namespace.

- Create and patch refuse an id another source or output holds with 409
  and a message naming that document. Like listener ports, create checks
  again after the write: a clash undoes the write, then a generated
  (auto) id is regenerated and retried, an explicit one is refused.
  A doc's own id never counts against it.
- Explicit ids must be [A-Za-z0-9_-]{16,64}; a shorter one is a 400 that
  says why (anyone who guesses the id can connect to the public relay
  port). auto is unchanged and generates 22 characters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(srt): one resolver for SRT ports and relay ids; non-blocking relay status

Code-review follow-ups on the SRT relay branch:

- One resolver, resolveSrtAddress() (next to resolveListenerAddress), and one
  scan, usedSrtBindings(), for listener ports and relay ids alike; the
  resolve/write/re-check/undo loop is shared by sources and outputs, create
  and update (services/srt-address-write.ts). relay-ids.ts is folded in.
- PATCH resolves and checks the EFFECTIVE address whenever the address or the
  stream type changes, so a whip -> srt patch can no longer store a literal
  `auto` relay id or one another document holds.
- PATCH re-checks after the write like POST, and undoes the write (restoring
  the previous document) on a clash.
- The relay status is stale-while-revalidate: requests are served from cache
  and refreshed in the background, the cache is warmed at startup, and only a
  lookup with nothing cached waits (at most 3 s).
- Strom unreachable is no longer "not configured": the status keeps the last
  known values with state unreachable/unknown; server-info carries `state`,
  and connect.reason says srt-relay-unreachable.
- Create/patch to a relay address is refused with 422 when Strom positively
  reports no relay; accepted while Strom cannot be asked.
- srtUrl() matches the srt+relay:// scheme case-insensitively, like the
  parser and Strom.
- srt-connect uses one host:port parser for authored URIs and the relay's
  public address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(srt): refuse new SRT addresses that GStreamer would read differently

GStreamer's SRT elements in Strom match query keys and mode values
exactly and drop most problems silently: `PassPhrase=` or `mode=Caller`
is ignored (an address that looks encrypted runs unencrypted), a bad
escape or %00 sets nothing, and a raw '#' cuts the URI. Strom now
refuses such a URI when it builds the flow, so Open Live accepted it on
save and the production failed only at activation.

- lib/srt-uri-check.ts: checkSrtUriAsGStreamerReads, a port of Strom's
  validate_srt_uri (types/src/srt_uri.rs): scheme exactly srt:// (or
  srt+relay://), no raw '#', no malformed escape or %00, keys and mode
  values in GStreamer's spelling (the message names it), known mode, no
  key given twice, passphrase 10-80 decoded UTF-8 bytes, pbkeylen
  0/16/24/32.
- srtUrl(url, { newValue }) runs it on a value being written: create,
  and a PATCH that sends address/url. A PATCH without one (rename)
  checks the stored address only by the older rules, so existing
  documents keep working.
- The passphrase limit is 80 bytes (libsrt HAICRYPT_SECRET_MAX_SZ), not
  79, and is measured decoded everywhere, so %20 counts as one byte. A
  stored passphrase that passed the old rule (length as written) still
  passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(srt): accept ! * ' ( ) ~ in an SRT address query

encodeURIComponent leaves these RFC 3986 unreserved/sub-delim characters
unescaped, so the studio (and gateways, Companion) sent a passphrase
such as "my!secret(2024)" as written, and the query allowlist answered
400 "Invalid SRT relay address format" with no hint at the cause. Both
srt:// and srt+relay:// now share one allowlist that includes them.
GStreamer and Strom pass them through unchanged, and the address only
travels as JSON. Double quote, backslash, space and angle brackets stay
refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(srt): refuse an SRT query key without a value

A query key without '=' (srt://:5000?latency) crashes GStreamer 1.28.6
when the SRT element starts, and Strom now refuses such an srt_uri when
it builds the flow. checkSrtUriAsGStreamerReads refuses it on save too,
naming the key, for srt:// and srt+relay:// alike, so a new address
never reaches Strom in that form.

The message for a mode in the wrong case follows Strom's correction:
GLib 2.88 and later read it ignoring case, older GLib ignores it, so
the mode would differ between builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(srt)!: read Strom's capabilities; named SRT relays; report SRT modes

Strom replaced GET /api/srt-relay (never released) with
GET /api/capabilities, which reports the SRT modes that can work, the
SRT relays by name with their public address, inbound reachability and
STROM_PORTS. Relay URIs gained a relay name: srt+relay://<id>[@<relay>].

- services/strom-capabilities.ts replaces srt-relay-status.ts: one
  cached, non-blocking lookup of GET /api/capabilities, same timings
  (30 s fresh, stale-while-revalidate, 5 s after a failure, 3 s first
  wait, warmed at startup). State ok / unsupported (404: an older
  Strom, every mode and no relay) / unreachable (last known values) /
  unknown. Parsing fails open: a mode is off only when Strom says
  false; malformed relays and port spans are dropped.
- Named relays: @<relay> is parsed, validated ([a-z0-9-]{1,32}) and
  written (the default relay without @, as Strom writes it). `auto`
  keeps the stored id and, without @, the stored relay, so renewing the
  passphrase never moves the stream; auto@<relay> moves it, auto@default
  moves it back. The dial-in address uses the named relay's public
  address; connect gains `relay`, and reason `srt-relay-unknown` for a
  name Strom does not report. A definite answer that the relay is not
  there refuses the write with 422 and `code`
  (srt-relay-not-configured | srt-relay-unknown) and `relays` (the
  configured names). Relay ids stay unique across all relays.
- #628: server-info `srt { state, modes { listener, caller, rendezvous },
  relays [{ name, public }], inboundReachable, ports }` replaces the
  unreleased `srtRelay`. Modes are reported, never refused: a listener
  or rendezvous address on a Strom that says it cannot work is stored
  and gets connect.warning (srt-listener-unsupported /
  srt-rendezvous-unsupported). Strom's own host is still resolved from
  the authored host, SRT_PUBLIC_HOST, then the STROM_URL host. The
  legacy port reservation is unchanged.
- docs/openapi.yaml documents the new fields, reasons and 422 body.

BREAKING CHANGE: server-info `srtRelay` is replaced by `srt` (only the
draft studio branch read it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): note how Strom's capabilities API landed for #628

ADR-014 and the SRT direction spec were written before Strom settled on
GET /api/capabilities with SRT modes and named relays, and before
GET /api/srt-relay was dropped. Add a dated note on what #629 now
implements where it differs (one endpoint, server-info `srt` shape,
modes reported rather than refused, relay 422 codes) and rename the
Strom env vars (STROM_SRT_RELAY_PUBLIC, STROM_SRT_RELAYS).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(srt): keep the stored passphrase when a relay address moves

A patch with srt+relay://auto[@<relay>] on a document that already holds a
relay address now keeps the stored (encrypted) passphrase, and the stored
pbkeylen, when the query has no passphrase key. The passphrase protects the
stream end to end and the relay never sees it, so moving to another relay
no longer makes the operator type it again. An empty passphrase= removes
it; passphrase=<value> sets it as before. An empty passphrase= is dropped
from every relay address before it is stored, so it never reaches Strom.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(srt): warn when a listener port is outside Strom's ports

Add connect.warning `srt-port-outside-strom-ports` for a plain srt://
listener whose port is outside every span Strom reports in
network.ports (last-known while Strom is unreachable), when no legacy
srtPorts reservation is active. `srt-listener-unsupported` wins when
Listener itself is unsupported. #630 resolution 9, required by the
team-lead sign-off on #629.

Tests cover the span checks, the reservation and null-ports cases, and
that create (201) and PATCH (200) on sources and outputs carry
connect.warning. openapi documents the new value on all four routes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): record the #629 amendment in the spec and ADR-014

Spec: status Implemented, Strom >= 0.8.2 (strom#1260/#1262), stale
strom#1257/#1258 and draft references fixed, /api/srt-relay marked
removed, superseded sections marked inline, #630 resolutions 11-14
folded in with the team-lead sign-off decisions.

ADR-014: Accepted (amended 2026-11-13); Decision 4 and the "Hide only
in the UI" alternative rewritten to record why modes are reported and
only relays are refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): mark the superseded #628 spec passages inline

Bring §13 T1/T3-T7, the §6 studio bullets, the §8 sequence diagram, §3.2, §4.1-§4.3, §5.1, §9 and the §2 table rows in line with what #629 ships: one fetch of GET /api/capabilities, modes reported on connect.warning (srt-listener-unsupported, srt-port-outside-strom-ports), no srt-direction-unsupported 422 and no srt-capabilities-unknown. Mark ADR-014 Decisions 1-3 as amended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: eyevinn-developer[bot] <343978855+eyevinn-developer[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants