Skip to content

TAMS Output block with OSC PAT/SAT auth - #647

Merged
srperens merged 17 commits into
mainfrom
feat/tams-output
Jun 26, 2026
Merged

TAMS Output block with OSC PAT/SAT auth#647
srperens merged 17 commits into
mainfrom
feat/tams-output

Conversation

@srperens

Copy link
Copy Markdown
Collaborator

What

Adds a TAMS Output block that records pre-encoded video/audio into a Time-Addressable Media Store via an Eyevinn TAMS Gateway, and the OSC PAT→SAT authentication it needs to talk to an OSC-hosted gateway.

TAMS Output block

  • New builtin.tams_output block. Two container modes: separate single-essence MP4 flows (default) or one muxed MPEG-TS format:mux flow.
  • GOP-aligned splitmuxsink segments, per-fragment TAI timeranges, deterministic (UUIDv5) source/flow ids so flow restarts append to the same timeline.
  • Background uploader keeps all HTTP off the GStreamer streaming threads; failed segments are kept on disk, successful ones deleted; shutdown flushes the tail fragment.
  • strom-types: shared TAMS protocol constants + timerange helpers, and TamsSegmentRegistered / TamsError WebSocket events.

OSC authentication (PAT → SAT)

OSC isn't plain OIDC: a long-lived Personal Access Token (PAT) is exchanged for short-lived (~1h), per-service Service Access Tokens (SATs).

  • backend/src/osc: reusable SatProvider that mints/caches SATs from a PAT (token.svc.prod.osaas.io/servicetoken), refreshing ~50 min in.
  • backend/src/client_auth: a general outbound AuthMethod enum (None / Bearer / OSC PAT-SAT), distinct from inbound auth.rs, extensible to Basic/OIDC. The TAMS client delegates all auth to it.
  • TAMS block gains an Authentication property: Static API Token (gateways outside OSC) or OSC PAT/SAT.

Multi-tenant PATs

A single shared Strom instance can serve multiple OSC tenants who must not reach each other's gateways.

  • PATs are keyed per flow (the flow id); STROM_OSC_PAT env seeds an instance-wide default fallback for the single-tenant case.
  • SAT cache keyed by (pat_fingerprint, service_id): same PAT shares a SAT per service, distinct tenants never do, and one PAT maintains a distinct SAT per service.
  • Runtime API (behind the auth middleware) so a control plane (Open Live) can push per-flow PATs to a freshly provisioned instance: PUT/DELETE /api/osc/pat/{key}, GET /api/osc/pat (status; never returns token values).

Misc

  • Load a local gitignored .env at startup (e.g. STROM_OSC_PAT).
  • Stop spamming thread-priority warnings every segment: splitmuxsink spins up a fresh muxer thread per fragment, so the "failed to set High priority" warning fired every ~2s without CAP_SYS_NICE. Now logged once per flow (with a setcap hint), then debug.
  • Uploader progress surfaced at info (flow created + every 30th segment) so a healthy uploader isn't silent at the default log level.

Testing

  • Unit tests for timerange formatting, deterministic id derivation, OSC service-id derivation, and the per-flow/default PAT resolution + fingerprint cache keying.
  • cargo test --test openapi_test updated (new /api/osc/pat endpoints + SetOscPatRequest / OscAuthStatusResponse).
  • Verified end-to-end against an OSC-hosted gateway: SAT minted, flow created, 181 segments registered on the timeline.

🤖 Generated with Claude Code

srperens and others added 14 commits June 24, 2026 16:44
Add a `builtin.tams_output` block that records pre-encoded video/audio
into a Time-Addressable Media Store via an Eyevinn TAMS Gateway.

- backend/src/tams: gateway HTTP client (presigned PUT/GET, flow/segment
  registration) + background segment uploader task (retry, keep-on-fail,
  shutdown tail flush) that keeps all HTTP off the GStreamer threads.
- backend/src/blocks/builtin/tams_output.rs: the block. Two container
  modes — separate single-essence MP4 flows (default) or one muxed
  MPEG-TS format:mux flow. GOP-aligned splitmuxsink segments, per-fragment
  TAI timerange capture, deterministic (UUIDv5) source/flow IDs so flow
  restarts append to the same timeline.
- strom-types: shared TAMS protocol constants + timerange helpers, and
  TamsSegmentRegistered / TamsError WebSocket events.
- Static API token support (gateway API_TOKEN) via the api_token property
  or STROM_TAMS_API_TOKEN; OSC SAT auth is a follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
OSC services authenticate with short-lived Service Access Tokens (SATs)
minted from a long-lived Personal Access Token (PAT) — not a plain OIDC
flow. A SAT lives ~1h and is scoped to a single OSC service type.

- backend/src/osc: a reusable, OSC-wide SatProvider that exchanges the PAT
  (STROM_OSC_PAT, or OSC_ACCESS_TOKEN as fallback) for SATs at
  token.svc.prod.osaas.io/servicetoken, caching one per service id and
  refreshing ~50 min in. derive_service_id() pulls the service-type slug
  from an *.osaas.io gateway URL. Process-wide, lazy from env.
- backend/src/client_auth: AuthMethod, a general *outbound* HTTP auth enum
  (None / Bearer / OSC PAT-SAT) that signs a reqwest request in apply().
  Distinct from auth.rs (inbound server auth); extensible to Basic/OIDC by
  adding a variant + arm. The TAMS client delegates all auth to it.
- TAMS Output block: a new "Authentication" property — Static API Token
  (default; for gateways run outside OSC) or OSC PAT/SAT. OSC mode derives
  the service id from gateway_url or takes an explicit OSC Service ID.
  Static token support is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Static API Token is a well-known mechanism — don't over-explain it. Keep
the explanation for OSC PAT/SAT, which is the unusual one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Let a control plane (e.g. Open Live) push its Personal Access Token to a
running Strom instance instead of requiring it in an env var at boot.

- osc: the SAT provider's PAT is now runtime-mutable (RwLock) and the
  process-wide provider always exists, seeded from env if present.
  set_pat/clear_pat replace the PAT and flush cached SATs; the env var
  remains the bootstrap path.
- api/osc: GET/PUT/DELETE /api/osc/pat (behind the auth middleware) to
  check status, set, and clear the PAT. Status never returns the token.
- TAMS Output OSC mode no longer hard-fails when no PAT is set yet — it
  warns and proceeds, so a flow can start before the PAT arrives; uploads
  surface a clear TamsError until it does.
- strom-types: SetOscPatRequest / OscAuthStatusResponse (+ openapi.json).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Auto-load a gitignored .env on boot (e.g. STROM_OSC_PAT) so OSC and other
credentials can be set per-checkout without exporting them by hand. Real
env vars still win; a missing .env is a no-op. Also gitignore .env/.envrc
so secrets can't be committed by accident.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A healthy uploader logged only at debug, so at the default info level it
looked silent after startup. Promote flow creation to info and log a
progress pulse (first segment, then every 30th) so operators can see
segments being registered without enabling debug.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
splitmuxsink spins up a fresh muxer thread per fragment, so each segment
rotation emitted a new StreamStatus::Enter and a fresh "failed to set High
priority" warning — every 2s for the life of a recording when the process
lacks CAP_SYS_NICE. Log the failure once per flow (with the setcap hint),
then drop to debug for the identical failures that follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A single global PAT breaks when one Strom instance is shared by multiple
OSC tenants: they must not reach each other's gateways, and the SAT cache
(previously keyed only by service id) could hand one tenant another's SAT.

- SatProvider holds PATs per credential key (the flow id), with the
  env-seeded STROM_OSC_PAT as a fallback default for the single-tenant case.
- token() resolves the flow's PAT (own, else default) and caches the SAT by
  (pat_fingerprint, service_id): flows sharing a PAT share one SAT, distinct
  tenants never do, and the same PAT across different services maintains a
  distinct SAT per service.
- AuthMethod::Osc carries the credential_key; the TAMS block fills it with
  the flow id at pipeline start (the only point the flow id is known) and
  warns if no PAT is available for that flow yet.
- API is per-flow only: PUT/DELETE /api/osc/pat/{key} (+ GET status). A
  control plane (Open Live) can always push per-flow without knowing whether
  the instance is shared; the global default stays bootstrap-only (env).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This intermediate fallback log fired per streaming thread — every segment
rotation with splitmuxsink — so it spammed every ~2s alongside (and despite)
the now-once-per-flow handler warning. It's an implementation detail; the
meaningful, actionable message is the handler's log-once failure with the
setcap hint. Demote to debug.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Guard the segment timeline against backward PTS jumps (live-source
  reconnect / encoder restart): re-anchor to current TAI instead of
  emitting overlapping, non-monotonic timeranges.
- Consume the caps-probe one-shot guard only after a parser is actually
  linked, so a first rejected/failed Caps event no longer permanently
  disables a later valid one (which left the flow recording zero segments).
- Delete the dropped fragment file on uploader-channel backpressure so it
  doesn't leak in the temp dir.
- OSC SatProvider: release the SAT cache lock before the network mint so a
  slow/hung token endpoint can't stall unrelated callers or PAT pushes.
- Uploader: read the segment off disk once instead of on every retry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MPEG-TS (one muxed format:mux flow with inherent A/V sync) is the more
broadcast-native default than separate MP4 flows. Reorder the dropdown so
the default appears first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surface the segment byte size at debug right before the presigned PUT,
and include it in the error log when an upload fails after retries. Makes
gateway body-size limits (e.g. nginx 413 Payload Too Large) immediately
diagnosable without packet capture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… guard overflow

Three fixes from the PR review:

- retry(): a gateway/storage HTTP 4xx (except 408/429) can never succeed on
  retry, yet the uploader slept through the full backoff and re-attempted 3x —
  producing repeated identical errors (e.g. the 413 Payload Too Large we hit).
  Errors now carry the status via a typed HttpStatusError and retry() gives up
  immediately on non-retryable ones.
- Clear leftover seg_* files from a previous run at setup. Permanently-failed
  uploads are kept on disk but nothing reads them back, and there is no teardown
  hook, so they accumulated across restarts. Cleaning at setup (before the new
  run writes anything) bounds disk use without touching keep-on-failure.
- max-size-time now uses saturating_mul, matching tail_segment_ns, so a large
  operator-supplied segment_duration_secs can't overflow u64.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
srperens and others added 3 commits June 25, 2026 17:47
…source_collection)

MP4 mode now records the canonical TAMS multi-essence model: each essence is a
mono-essence Flow under its own mono-essence Source, grouped by a Multi-Flow
(NMOS format:multi) whose flow_collection lists the members by role. The gateway
derives the multi-Source's source_collection from this. MPEG-TS mode is unchanged
(one opaque format:multi flow).

- FlowSpec: codec/container are now Option (a grouping Multi-Flow carries no
  essence), and a flow_collection field carries (flow_id, role) members.
- ensure_flow body building extracted to a pure flow_request_body() and unit
  tested: codec/container omitted when None, flow_collection members serialized
  as { id, role, container_mapping: { track_index } } (gateway requires the
  mapping), tags as an object.
- MP4 mode: per-essence mono-Sources (video-source/audio-source) so the gateway
  resolves each member flow to a distinct Source when deriving source_collection;
  the Multi-Flow keeps the program Source.
- register_multiflow re-PUTs the Multi-Flow on a short schedule so a PUT lands
  after the lazily-created member flows exist (the gateway derives at PUT time
  and does not re-derive on later member registration).
- Use NMOS format URN urn:x-nmos:format:multi (TAMS content-format value), not
  the AMWA :mux; renamed FORMAT_MUX -> FORMAT_MULTI.

Verified end-to-end against a live Eyevinn TAMS gateway: the Multi-Flow's
flow_collection and the derived source_collection (video + audio mono-Sources by
role) round-trip correctly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A TAMS Output flow can now carry several audio tracks (e.g. language /
commentary / described-audio), capped only by a sanity bound (32). Video stays
a single track: splitmuxsink exposes one `video` pad but `audio_%u`, so a mux
physically holds one video and many audio essences.

- Essence::Audio carries a track index; input/element ids and the collection
  role/tag are indexed (audio_input_0/1, role audio-0/1).
- get_external_pads emits audio_in_0..N; build wires N audio essences.
- MPEG-TS mode muxes all audio into the one format:multi flow; MP4 mode gives
  each audio its own flow + mono-Source, all grouped in the Multi-Flow's
  flow_collection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…very sweep

Recording survives a busy/over-quota gateway and even a full Strom restart.

- Inline attempts raised 3 -> 6 with exponential backoff (1,2,4,8,16,32s)
  clamped to 30s, plus up to ~1s of dependency-free jitter so concurrent
  uploaders don't retry in lockstep against a stressed gateway.
- A segment that fails its inline attempts is parked on disk: its bytes are
  kept and a `.meta` sidecar persists its TAI timerange. A background recovery
  sweep (every 30s, and once at uploader start) re-registers parked segments
  once the gateway recovers — across restarts too, since the timerange is on
  disk. Non-retryable (4xx) rejections are dropped with an error instead.
- Startup no longer blanket-deletes leftover segments; it removes only orphans
  (no sidecar, unrecoverable) and leaves parked segments for the sweep.
- Uploader internals refactored around an UploadCtx; sidecar round-trip and
  backoff clamping are unit-tested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@srperens
srperens merged commit b3da495 into main Jun 26, 2026
8 checks passed
@srperens
srperens deleted the feat/tams-output branch June 26, 2026 10:16
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.

1 participant