Repository navigation
Conversation
ec3c766 to
e371ecd
Compare
|
This PR looks like the base of a multi-PR dependency stack for the low-latency fast return feed, together with #393 (play the fast return feed on the guest page, which depends on this PR). Holding off on automated review/merge here — this needs a human (or the relevant dev agent) to assess the whole stack together, not a per-PR pass. Nothing further is needed from the author; this comment exists so the dependency is recorded on the PR rather than in a run log. |
Conflicts from 6ad0d3f (fast feed follows a mixer read) resolved to Eyevinn#386's side: Eyevinn#386's direct-outs refactor replaced that approach and removed fast-feed-state.
The fixture repeated makeActivationResult's field list. A branch that adds a field to makeActivationResult (as Eyevinn#386 does) then merged into whichever copy git matched, and left the other without it. Spreading the base keeps one list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bc1a39f to
2618709
Compare
The fixture repeated makeActivationResult's field list. A branch that adds a field to makeActivationResult (as Eyevinn#386 does) then merged into whichever copy git matched, and left the other without it. Spreading the base keeps one list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixture repeated makeActivationResult's field list. A branch that adds a field to makeActivationResult (as Eyevinn#386 does) then merged into whichever copy git matched, and left the other without it. Spreading the base keeps one list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixture repeated makeActivationResult's field list. A branch that adds a field to makeActivationResult (as Eyevinn#386 does) then merged into whichever copy git matched, and left the other without it. Spreading the base keeps one list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A guest's picture feed plays its mix-minus about 0.8 s after the voices it carries were spoken: the WHIP jitterbuffer (400 ms) plus the program flow's latency, because Strom holds every sink in a flow to one latency. That is too slow to talk over. For a guest slot with `returnFeed.lowLatency: true`, activation now also builds the spec's fast feed. The audio mixer is built with `direct_outs` (Eyevinn/strom#930), and each channel's direct out (after its fader, mute and to-Main) feeds a Strom audio bridge in the program flow. A second, conversation flow takes the bridges into a liveaudiorouter, a fixed mix-minus that leaves out each guest's own channel, and feeds an audio-only WHEP output per guest. The bridge restamps on arrival and holds a small target latency, so neither the jitterbuffer nor the program's latency reaches the fast feed. Crew changes reach it inside Strom; open-live never writes the router after activation. - Activation stores `fastWhepUrls` and `fastFeedRouter`; deactivate clears them, and an activation without a fast feed overwrites what an idle or reconcile stop left behind. - The conversation flow is tagged `conv:<programFlowId>`. `deactivateStromFlow` removes it with its program flow; activation and startup reconcile remove one whose program flow is gone. - If the conversation flow cannot start, the production still activates, without fast feeds. More than 8 audio channels (the router's limit), or a channel with no source link, builds no fast feed. - `values.fast_return_latency_ms` sets the bridge target; Strom's default otherwise. The API still accepts only `lowLatency: false`; the next commit serves the feed and accepts `true`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A guest slot can now be declared with `returnFeed.lowLatency: true`, and its fast feed is served like the picture feed. - `POST/DELETE /api/v1/productions/:id/returns/:mixerInput/fast/whep` (crew, and guest token via the existing guest scope) and `/api/v1/guests/:inviteId/returns/fast/whep` (guest alias, reachable through the OSC ingress gate) proxy the fast feed through the same helpers as the picture feed. - A guest's fast-feed WHEP session is bound to its own `fastWhepSessionId`, so neither feed's session id tears down the other. The binding re-reads the guest session and retries a 409, since a client opens both feeds at once. - Join, the crew return view and `GET .../session/return` list the `fast` feed (`video: false`) and the `low-latency-minus` mode when the guest has a fast feed. Join hands out the guest-alias URL. `PUT .../session/return` still 400s `low-latency-minus`: it is a client-side feed choice, not a mix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2618709 to
3b92db6
Compare
|
Claude (Claude Code, for @wagenet): rebased onto main ( |
|
This PR is part of the low-latency return-feed feature on Members:
Nothing further is needed from the author; this records the dependency on the PRs themselves. |
* fix(recording): link PGM video to the recorder's video_in_0 pad
Strom's builtin.recorder numbers its inputs from zero (video_in_0,
audio_in_0). The generator linked PGM to `video_in`, which the recorder
does not have. Strom drops that link as stale when the flow is created,
so the recorder was built with its video track unconnected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(recording): set the recorder's filename_prefix, not prefix
The recorder's property is `filename_prefix`. Strom ignores the unknown
`prefix`, so segments were named recording_00001.mp4 instead of
<production id>_00001.mp4. Output dir and upload key already carry the
production id, so nothing else changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(recording): AAC-encode the main mix before the recorder
The recorder refuses raw audio ("Recorder needs AAC, MP3, AC-3, DTS or
Opus audio, but its audio input is raw"), and the audio mixer's main_out
is raw, so the recording had no audio track. Route main_out through a
builtin.audioenc (AAC) into the recorder's audio_in_0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(recording): match the recorder's filename template in comments
Strom writes {filename_prefix}_{timestamp}_%05d.{ext}. Update the three
comments that describe the segment filename.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(recording): record picture only when Strom has no audio encoder
builtin.audioenc first shipped in Strom 0.6.9. An older Strom refuses to
start any flow that references it, so a production with a recording
output could not go on air. Activation now checks Strom's block list
and leaves the encoder out when the block is missing. The recording then
has picture but no sound, and activation saves a warning on the
production (activationWarnings) and logs it. If the block list can't be
read, activation keeps the encoder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(recording): show activation warnings as a toast in the controller
Studio shows WS ERROR frames as a toast, so activation warnings (such as
a recording with no sound) now go out that way: broadcast to connected
controllers when the production activates, and sent again to each
controller that connects while the production is live.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(recording): clear activation warnings when activation fails
A production that never went on air was left inactive with the warning
from the failed attempt still set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(recording): send activation warnings once the production is live
The warning toast is broadcast when the production goes active, and
not at all if activation was aborted. The connect-time frame needs
status 'active' as well as a stromFlowId, so an activating production
or one left with a stale flow id sends nothing. controller-websocket.md
documents the ERROR frames that carry activation warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(recording): build the warning fixture from makeActivationResult
The fixture repeated makeActivationResult's field list. A branch that adds
a field to makeActivationResult (as #386 does) then merged into whichever
copy git matched, and left the other without it. Spreading the base keeps
one list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(recording): update main's tests for warnings and video_in_0
activation-failure-relay-stop's makeActivationResult now returns
warnings: [], which runActivationFlow reads. flow-generator-encoders
looks for the recorder feed on video_in_0. activation.test's
makeActivationResult keeps main's mixerInputMap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(strom-public-url): add warnings to the activation mock
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Jonas Birmé <jonas.birme@eyevinn.se>
Opened by Claude (AI) on Peter Wagenet's behalf.
Depends on Eyevinn/strom#844 (audio bridge blocks) and Eyevinn/strom#930 (mixer direct outputs). Do not merge until both are merged and deployed. The program flow uses both, so on a Strom without them a production with a
lowLatencyguest fails to start. Productions without one are unaffected.What
returnFeed.lowLatency: truenow builds the fast feed the guest-calling spec describes under "Low-latency mode": an audio-only mix-minus that reaches the guest about 500 ms before the picture feed's audio.Program flow: the audio mixer is built with
direct_outs: true, and each channel'sdirect_out_Nfeeds abuiltin.audio_bridge_output. A direct out carries what the channel sends to Main: after the fader, the mute and audio-follow-video (ch{N}_to_main), before the Main sum. Crew changes therefore reach the fast feed inside Strom, with the same fades as program.Conversation flow (a second Strom flow): one
builtin.audio_bridge_inputper channel feeds abuiltin.liveaudiorouter. The router is a fixed mix-minus set at activation: each guest's output sums every channel except their own. Each output goes to an audio-onlybuiltin.whep_output(whep-fast-<N>-<suffix>). Router settings are 15/10/5 ms, the values measured with five live seats. The bridge target is Strom's default (40 ms), orvalues.fast_return_latency_msif set.Limit: Strom's router takes at most 8 inputs. A production with more than 8 audio channels, or with an audio channel that has no source, gets no fast feed and a warning; its guests keep the picture feed's audio.
API:
returnFeed.lowLatency: trueis accepted onPOST /productions/:id/sources(main accepts onlyfalse). The fast feed is served wherever the picture feed is, through the same proxy helpers:POST/DELETE /api/v1/productions/:id/returns/:mixerInput/fast/whepPOST/DELETE /api/v1/guests/:inviteId/returns/fast/whep, the alias the OSC ingress gate lets throughA guest's fast session is bound to its own
fastWhepSessionId, so neither feed's session id tears down the other. Join, the crew return view andGET /guests/:inviteId/session/returnlist thefastfeed (video: false) and thelow-latency-minusmode only when the guest has one. Join hands out the guest-alias URL.low-latency-minusis still refused by the mode PUTs: it is a feed choice for the client, not a mix.Lifecycle: the conversation flow is tagged
conv:<programFlowId>and is removed with the program flow. Activation and startup also remove any conversation flow whose program flow is gone.Why a second flow, and why direct outs
Strom runs every sink in a flow at one latency. On a TRL-shaped production that is 360 ms, set by the mixer's monitor bus (Eyevinn/strom#920 cuts it). It sits on top of the 400 ms WHIP jitterbuffer offset. A return inside the program flow cannot be released earlier than that. The bridge restamps each buffer on arrival, so neither cost reaches the conversation flow.
Tapping each source before the mixer would need every crew change copied into the router's gains, and that copy cannot be kept correct from outside the mixer: writes to the mixer and to the router can be answered out of order or refused separately, and reading the mixer back returns live element values, which are 0 for a muted fader and stale for a stalled source. Fed from the direct outs, the router never changes after activation.
A direct out is not an aggregator output. It taps the channel strip before the Main sum, so one guest's bad link is not passed to the other guests through the mixer.
A running program flow is never edited
Per strom#930, a direct-out consumer that ends (EOS) or is torn down (flushing) while program runs silently stops that channel on program. open-live builds every
audio_bridge_outputinto the program flow at activation and never edits a running flow:flows.updateis unused, and the program flow is only stopped and deleted whole. The bridge sink is unsynchronised and returns OK whether or not anyone reads, so stopping the conversation flow does not reach program.Measured
Rig: a fresh Strom per trial (Strom main with #844, plus #930), activated through this branch's
activateStromFlow. Five WHIP seats (Opus + H.264), loopback. Delay is from tone ticks into seats 1–4 to the same ticks out of seat 0's feeds, all on one clock, through a GStreamer WHEP consumer. Over 4 trials at load average 6–8, the fast return was 188 ms (trial means 175–200 ms) and the picture return 688–703 ms. Worst dropout was 0.17 %.Crew changes, with open-live not running: a mute on one channel,
to_mainoff on another, and a fader at 0.1 on a third, each sent straight to Strom's mixer for 3 s. Each took that seat's ticks in seat 0's fast feed from 0.40–0.55 down to 0.02–0.08, and each came back when undone. Seat 0's own voice stayed at the noise floor throughout. Two runs, same result.One bad link: one seat had 0–200 ms jitter, 0.4–1.2 s stalls and 14 % burst loss. A tone from another seat lost 0.14–0.17 % in seat 0's fast feed, with no change in delay. Three alternated control runs lost 0.13–0.21 %. Leave and rejoin: a guest left, then published again on the same slot 4 s later. They were back in the fast feed and on program by the next tick. Two runs, and the other seats were undisturbed.
These runs predate the rebase onto current main. The fast-feed flow code is unchanged since; the program flow around it now has main's shared viewer encode (#413) and compact vision-mixer pads (#463). The fast feed is keyed by audio channel, and its endpoint ids use the stored
video_in_N, as the picture feed's do, so #463's pad map does not reach it.Not in this PR
fastFeedRouteris stored on the production but nothing reads it yet.How to review
Two commits, each builds and passes on its own:
feat(returns): build the low-latency fast return feed:src/lib/fast-returns.ts(the plan) andsrc/lib/flow-generator.ts(wiring it into activation and teardown), plus storage inproductions.tsand the orphan sweep inreconcile.ts. The API still accepts onlylowLatency: falsehere.feat(returns): serve the fast return feed and accept lowLatency: true:src/routes/returns.tsturns main's picture-only proxy helpers and routes into ones that take a feed;guests.tsadds the fast feed to join;server.tsandguest-scope.tswiden the guest-token path patterns frompicturetopicture|fast.Tests
flow-generator-returns.test.ts: the mixer getsdirect_outs: trueonly when fast returns are built; each bridge links fromdirect_out_<ch+1>and nothing else; the router matrix is the fixed mix-minus; no fast feed past 8 channels; the program keeps running if the conversation flow fails; teardown removes only this program's conversation flow.returns-guest-auth.test.ts: crew and guest-alias fast routes, separate session binding for the two feeds, a 409 retry on the binding, and the feed and mode listings.activation.test.ts,production-lifecycle.test.ts,guest-slot-declaration.test.ts: storage, the startup sweep andlowLatency: trueon the sources route. The activation test's controller mock now stubsreinitConnectedControllers, which main's activation calls once the flow plays.pnpm typecheckandpnpm testpass on each commit: 1056 tests after the first, 1065 after the second (105 files).Not run since the rebase: the Strom rig, a real remote guest, a browser.
🤖 Generated with Claude Code