TAMS Output block with OSC PAT/SAT auth - #647
Merged
Merged
Conversation
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
force-pushed
the
feat/tams-output
branch
from
June 24, 2026 14:45
3052d90 to
f3bd551
Compare
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
builtin.tams_outputblock. Two container modes: separate single-essence MP4 flows (default) or one muxed MPEG-TSformat:muxflow.splitmuxsinksegments, per-fragment TAI timeranges, deterministic (UUIDv5) source/flow ids so flow restarts append to the same timeline.strom-types: shared TAMS protocol constants + timerange helpers, andTamsSegmentRegistered/TamsErrorWebSocket 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: reusableSatProviderthat mints/caches SATs from a PAT (token.svc.prod.osaas.io/servicetoken), refreshing ~50 min in.backend/src/client_auth: a general outboundAuthMethodenum (None / Bearer / OSC PAT-SAT), distinct from inboundauth.rs, extensible to Basic/OIDC. The TAMS client delegates all auth to it.Multi-tenant PATs
A single shared Strom instance can serve multiple OSC tenants who must not reach each other's gateways.
STROM_OSC_PATenv seeds an instance-wide default fallback for the single-tenant case.(pat_fingerprint, service_id): same PAT shares a SAT per service, distinct tenants never do, and one PAT maintains a distinct SAT per service.PUT/DELETE /api/osc/pat/{key},GET /api/osc/pat(status; never returns token values).Misc
.envat startup (e.g.STROM_OSC_PAT).splitmuxsinkspins up a fresh muxer thread per fragment, so the "failed to set High priority" warning fired every ~2s withoutCAP_SYS_NICE. Now logged once per flow (with asetcaphint), then debug.Testing
cargo test --test openapi_testupdated (new/api/osc/patendpoints +SetOscPatRequest/OscAuthStatusResponse).🤖 Generated with Claude Code