AI AI & agentic architectures

SFU, MCU, and the participant that has to decode

Who said what, and who holds the key, both depend on where the agent's audio is decoded.

On this page · 10 sections

Ana, Ben and Chloe are on a call planning a launch, and they’ve added an AI note-taker. At the end it should hand back the action items with a name on each: Ben: QA needs the build by Tuesday. Chloe: send it Monday. To do that, the model behind the agent has to know who said what. And the team’s calls are end-to-end encrypted, so somebody has to decide whether the agent gets the key.

Both questions have the same answer underneath: it depends on where in the call the agent’s audio gets decoded, turned from compressed bytes back into sound. Group calls of more than a few people usually run through one of two kinds of server, and the agent can sit in three different places around them. We’ll put the note-taker in each seat, hear what its model gets, count what each seat costs, and then work out who has to hold the keys. A small Python simulation I’ll quote throughout runs every case.

The short version

  • A server that only passes audio along can do its job without listening to it. A server that mixes voices together has to listen.
  • If the agent gets each person’s audio separately, the model knows who said what. If it gets one mix of everyone, it can’t tell who said “Tuesday”.
  • Where the agent sits decides which of those it gets, and what it costs: three options, three answers.
  • On an end-to-end encrypted call, a server that passes audio along still works without the key. The agent doesn’t: to hear anything, it has to be trusted with the key, and so does whoever gives it the key.

Three seats for a note-taker#

Picture a meeting room with a stenographer. There are three places to put them:

Where the stenographer sitsWhat they hearIn a call
With a headset per person, each wired to one microphoneEach person on their own channelThrough a forwarding server, an SFU (selective forwarding unit)
In the next room, listening to the room’s loudspeakerOne blend of everyoneAs a client of a mixer, an MCU (multipoint control unit)
At the mixing desk, where every microphone has its own channelEach channel, already turned into soundInside the mixer

The first and third seats keep the voices apart. The second gets them already added together, and adding is the one step you can’t undo.

If the two kinds of server are new to you, the voice series’ Where the server sits builds them up from scratch. Here’s what matters for the agent.

Unlocking is not decoding#

Ana’s voice travels as packets, one every 20 ms. Each packet has a header, a small label on the outside saying whose stream it belongs to (its stream ID, which RTP calls the SSRC), its number in the stream, and when it was recorded. After the header comes the audio itself, compressed by Opus, the codec browser calls use for voice.

The packet, field by field

The packets are RTP, the Real-time Transport Protocol. The fields this article leans on:

  • The SSRC, the stream ID: which stream the packet belongs to.
  • The sequence number, which lets the receiver spot a lost packet, or two arriving out of order.
  • The timestamp, which counts samples, 48,000 a second for Opus, so a 20 ms packet moves it on by 960.
  • Header extensions (RFC 8285), optional extra fields, such as the speaker’s loudness, which the forwarding server below reads.
  • The CSRC list, how a mixer names the streams inside a mixed packet, up to 15 of them.
  • The payload, the audio compressed by Opus. Decoding turns it into PCM samples, plain numbers measuring the sound wave.

Every packet crosses the network locked. Each hop, laptop to server and server to agent, has its own key, and the server at the end of a hop holds that hop’s key. That’s SRTP: it encrypts the audio and leaves the header readable, though tamper-proof. WebRTC, the standard browser calls use, requires it on every hop: all media channels “MUST be secured via SRTP” (RFC 8827 §6.5).

So every server that ends a hop unlocks every packet, because it has to lock it again for the next hop. (Where the server sits shows why, and has a lab where you try it.) But unlocking only gives you the compressed Opus bytes. Turning those into sound, a stream of numbers measuring the sound wave, is decoding, a separate step with a real cost. A server can unlock every packet and never decode one.

That distinction is the whole article:

Two rows of steps. SRTP encrypts the payload and only authenticates the header. A forwarding server reads the header, chooses who gets the packet by audio level, rewrites the sequence number and often the stream ID, and swaps hop keys, re-encrypting the payload; the audio inside passes through unchanged, and there is no decoder. A mixer reads the header, decrypts the payload, buffers by timestamp, decodes Opus to samples, mixes all but the listener, encodes a new stream, writes a new header with its own stream ID and the list of contributors, and encrypts it. Summing the samples throws away who contributed what. The decode, mix and encode steps, in amber, need the audio itself, so on an end-to-end encrypted call a box that does them has to hold the end-to-end key.
A forwarding server works on the header and never decodes. A mixer has to decode, and the amber steps are the ones that need the sound.

Remember the amber steps. They come back when we get to keys.

The forwarding server: choose by the header#

A forwarding server reads each packet’s header, decides who should get it, and sends the same audio on. It can even pick who’s speaking without hearing anyone: each sender writes its own loudness into one byte in an extension to the header, 0 for loudest and 127 for silence (RFC 6464). The open-source SFU mediasoup, which “does not transcode or mix media”, describes its version in seven words: “No decoding of audio data is done” (docs).

The simulation’s forwarder sends the agent the two loudest of Ana, Ben and Chloe. Skipping a quiet packet creates a problem first, though. Each stream the agent receives is numbered packet by packet, and a missing number reads as a lost packet. So when the forwarder skips one on purpose, it closes the gap by renumbering everything after it, but leaves a real network loss as a gap, so the agent’s decoder knows to cover for it:

python
call_topology/boxes.py
for receiver in receivers:
if receiver == sender:
continue
leg = (receiver, pkt.ssrc)
if pkt.ssrc not in loudest:
if leg in self.skipped: # leg already started
self.skipped[leg] += 1 # close the gap we made
continue
offset = self.skipped.setdefault(leg, 0) # a real loss stays a gap
out[receiver] = pkt.with_(seq=pkt.seq - offset).to_bytes() # payload unchanged

A leg is one sender’s stream on its way to one receiver, named by the receiver and the stream ID, ssrc. For each leg, skipped counts the packets the forwarder dropped on purpose, and every later packet is renumbered down by that count. A quiet packet only counts once the leg has started: before the first packet goes out, there’s no numbering yet to leave a gap in. The last line is the point: a new number, and the audio bytes untouched.

Here Chloe goes quiet for one 20 ms packet, at level 90:

text
python -m call_topology.demo forward --speaker Chloe
Chloe seq 5200 -> 5200 ts 0 payload bytes unchanged: True agent decodes Chloe: [300, 300, 300, 300]
Chloe seq 5201 level 90: not forwarded
Chloe seq 5202 -> 5201 ts 1920 payload bytes unchanged: True agent decodes Chloe: [300, 300, 300, 300]

Ana and Ben were louder (levels 10 and 12), so Chloe’s quiet packet was the one skipped. The agent receives packets 5200 and 5201, with no gap where it was. The timestamp still jumps from 0 to 1920, which is 40 ms at Opus’s 48,000 samples a second, so the agent knows time passed. (The simulation’s packets carry only 4 samples each, rather than a real packet’s 960, so each line fits on the screen.)

A packet lost on the network is different, and stays a gap:

text
python -m call_topology.demo loss
Ana seq 5000 -> 5000
Ana seq 5001 lost on the network
Ana seq 5002 -> 5002

The agent sees 5000 then 5002, knows 5001 is missing, and its decoder fills the 20 ms with a plausible guess, which Opus calls packet loss concealment.

What a real forwarder does beyond this

A real forwarder smooths the loudness over time rather than trusting one packet (RFC 6464 §5). In mediasoup, the AudioLevelObserver reports the incoming streams (mediasoup calls them producers) ordered by volume, “louder ones go first”, and the application pauses or resumes each receiver’s copy to match; a paused copy means “no RTP is sent to the consuming endpoint”. Many, mediasoup included, also give each receiver its own stream ID, and shift the timestamp by a fixed amount when they switch a receiver to another source stream, such as another quality layer of the same sender. None of it needs the sound. It’s the simple case of what RFC 7667, the IETF’s catalogue of call shapes, calls selective forwarding.

The mixer: add, and lose who said what#

A mixer has to open the audio. It decodes every input, lines them up in time (RTP’s own spec says the mixer “will make timing adjustments among the streams and generate its own timing for the combined stream”, RFC 3550 §3), and builds a mix for each listener that leaves the listener out, because hearing your own voice back is an echo. The simulation’s version:

python
call_topology/boxes.py
others = [ssrc for ssrc in frames if ssrc != listener] # never your own voice
length = len(frames[others[0]])
mixed = [clip(sum(frames[s][i] for s in others)) for i in range(length)]

frames holds every input’s decoded samples, keyed by stream ID. For each listener it keeps everyone else’s, then adds them up sample by sample, with clip keeping the sum inside what a sample can hold.

Put one 20 ms frame from each person in, plus the agent’s own reply, with each voice reduced to one number so you can do the sums by eye:

text
python -m call_topology.demo mix
inputs Ana 100, Ben 200, Chloe 300, agent 50
to Ana SSRC 0xe5 CSRC Ben, Chloe, agent samples [550, 550, 550, 550]
to Ben SSRC 0xe5 CSRC Ana, Chloe, agent samples [450, 450, 450, 450]
to Chloe SSRC 0xe5 CSRC Ana, Ben, agent samples [350, 350, 350, 350]
to agent SSRC 0xe5 CSRC Ana, Ben, Chloe samples [600, 600, 600, 600]
mixer decodes: 4, encodes: 4

Ana gets 200 + 300 + 50 = 550: everyone but herself. The agent gets 600, Ana + Ben + Chloe. Every mix comes from the mixer’s own stream ID, 0xe5, and its header carries a contributor list, the CSRC, naming who’s inside. But the 600 itself has no trace of which part was Chloe. Getting the voices back apart would take a separate model trained to untangle speech.

What RFC 7667 means by "mixer", and why it avoids "MCU"

RFC 7667, the IETF’s catalogue of call shapes, sets out to “address past and existing confusion” around industry terms “such as the Multipoint Control Unit or MCU”, which is why this article says forwarding server and mixer. It defines a mixer by what it sends: a middlebox that aggregates streams “by generating one or more new RTP streams”. That’s also why decoding doesn’t make the agent a mixer. It decodes everyone, but the only stream it sends is its own voice.

The mixer here is the media-mixing kind, the one most people picture. The RFC also describes a media-switching mixer, which “avoids the media decoding and encoding operations in the mixer” and forwards much as a forwarding server does, though it still unlocks and relocks every packet, because it rewrites the headers.

Where the agent sits#

Three lanes with the same three people. Through a forwarding server, the server decodes nothing, and the agent decodes the 3 streams itself and hears each person. As a client of a mixer, the mixer decodes 4 streams, the 3 people plus the agent's reply, and sends 4 mixes; the agent decodes 1 mix and hears one blend. Inside the mixer, the mixer decodes 3 streams and sends 3 mixes, and the agent, running inside the mixer, gets the 3 decoded voices and hears each person. Purple is the agent and its model. The agent's reply back into the call isn't drawn.
Three seats for the same agent. Only the middle one hands the model a blend.

Through a forwarding server, the agent is an ordinary participant. It subscribes to each person’s stream and decodes all three itself. This is the headset-per-person seat.

As a client of a mixer, the agent receives one mix of everyone but itself and decodes it once. A bot that dials into a conference bridge by phone sits here: the phone line carries one channel of sound.

Inside the mixer, the agent reads each person’s audio after the mixer has decoded it and before it’s added in, and hands its own reply back as sound. It’s the natural seat when you already run a mixer that decodes every stream.

Hear the difference. Pick a seat and play the meeting as the model receives it, then try muting one person:

What the agent hearslive lab

This lab runs a real call in your browser, so it needs JavaScript.

If you use LiveKit

LiveKit is a forwarding server, and its docs call an agent “just another participant in the room”. One trap for a note-taker: an agent session listens to one linked participant by default, the first to join, not to everyone. A group-call agent has to be set up to listen to every person, for example with one session per participant, as LiveKit’s multi-user transcriber example does.

Real examples of the mixer seats

Asterisk, the open-source phone server, can attach an app to a conference through an External Media channel, which “forwards all media from the bridge”: the mixer’s-client seat. It gets close to the inside seat from outside, too: its REST interface can snoop on one participant’s channel and send just that person’s audio out.

Why a note-taker wants each person#

For almost any agent in a group call, I’d want the per-speaker seats. Each stream belongs to one participant, which the call’s signalling, the separate channel where people join and describe their streams, says by name. With a stream per person, the model gets three things the blend can’t give it:

With one stream per personWith one blend
Who said what: “Tuesday” is on Ben’s streamHas to be guessed from the voice
When each person has finished talking, so the agent knows when to answerHarder to tell: when Ben stops and Chloe starts, the mix never goes quiet
One person’s background noise stays on their streamEveryone’s noise in one transcript

What each seat costs#

The simulation counts decodes for the same three people, assuming the forwarding server sends every stream to everyone, and the mixer decodes every input and sends each endpoint its own mix. (A mixer that mixes only the loudest few decodes less, and people who aren’t speaking can share a mix.)

Three people, one agentThrough a forwarding serverClient of a mixerInside the mixer
What the model hearseach person separatelyone mixeach person separately
Streams the agent decodes310
Decodes on the server043
Encodes on the server043
Decodes between a microphone and the model121
Decodes between the agent’s reply and a listener121
Decodes in the whole call1286

From python -m call_topology.counts --people 3.

The whole-call row adds the agent, the server and everyone’s laptops:

  • Through a forwarding server: 3 by the agent, none on the server, and each of the three people decodes the other two plus the agent’s reply, 3 × 3 = 9. 12 in all.
  • Client of a mixer: 1 by the agent, 4 by the mixer (three people and the agent’s reply), and 1 mix per person, 3. 8 in all.
  • Inside the mixer: none by the agent, 3 by the mixer, and 1 mix per person. 6 in all.

So the forwarding server costs the most decodes in total, but it spreads them across everyone’s laptops and the agent, and the server itself decodes nothing. The row that matters more for an agent is the one above it. Every decode sits behind a short wait for late packets, a jitter buffer, and every encode collects a full frame of sound first, so the mixer’s client pays both delays twice before the model hears anything. In the stenographer’s terms, the one in the next room hears the loudspeaker a moment after the room does.

The larger cost is usually further along. One stream per person means one speech-to-text session per person, three instead of one for the mix. Only sending a stream on while someone’s actually talking narrows that gap.

Who has to hold the keys#

Now the encryption. Every server that ends a hop holds that hop’s key (a TURN relay, which only passes packets along, holds none), which means that on an ordinary WebRTC call a forwarding server could decode your audio; it just doesn’t. RFC 7667 says it outright: a forwarder or mixer needs “to be able to decrypt the media to perform its function” (§6).

End-to-end encryption adds a second lock inside the first, with a key only the ends of the call hold: the people and machines trusted to hear it. The servers still take off the outer lock for their hop, but the sound inside stays sealed. In the stenographer’s terms, the room is soundproofed, and only someone with a key can listen in. The call’s key usually comes from a key service, part of the app, which hands it to each laptop as people join. The IETF’s standard for the inner lock is SFrame, which seals each frame, or each packet, so forwarding servers “can access the media metadata needed to make forwarding decisions without having access to the actual media”.

Go back to the amber steps in the figure. A forwarding server never does them, so it keeps working on an end-to-end encrypted call. A mixer does all of them, so it works only if it’s one of the ends. And the agent has to decode, so it has to hold the key too. The simulation seals one packet and hands it to each box (its cipher is a toy stand-in for SFrame, not real cryptography, and it leaves out each hop’s lock):

text
python -m call_topology.demo keys
forwarding server, no key: relays it, payload bytes unchanged: True
mixer, no key: recovers Ana's audio: False
agent with the key: hears ('Ana', [100, 100, 100, 100])
agent with the wrong key: wrong key: this box is not one of the ends

Now do it with real encryption, AES-GCM, one of SFrame’s cipher suites. Hand Ana’s frame to the forwarding server first, then the mixer, and see which one gets stuck; then try the agent with the right key and with a wrong one:

Who can open itlive lab

This lab runs a real call in your browser, so it needs JavaScript.

LiveKit’s guide to end-to-end encryption with agents describes the last case from experience: the agent “needs the shared encryption key”, and without it “The agent joins, but STT never produces transcripts, so the LLM never receives input and the agent never speaks.” (STT is speech-to-text; the LLM is the language model.)

So here’s who can hear what on an end-to-end encrypted call through a forwarding server:

BoxHolds the hop keyHolds the call’s keyCan hear the audio
Ana’s, Ben’s and Chloe’s laptopsyesyesyes
Forwarding serveryesnono; sees who, when and how loud
Agentyesyesyes
Key service that hands out the keyn/ayesyes, if it wants
Hosted speech-to-textn/anoyes: the agent sends it plain audio

Here’s the order things happen in. The key goes out first, then media flows:

Hosted STTForwarding serverAgentAnaKey serviceHosted STTForwarding serverAgentAnaKey servicewhoever runs the key service or the agentis an end of the callsees who, when, how loud,never the audioopens it and decodesoutside the ends,so users should be toldcall's key, on a channel the server can't readsame key, also out of the server's reachsealed framesame sealed frameplain audiotranscriptreply, sealedsame sealed reply

Of the boxes in the diagram, only the key service, Ana and the agent can open the audio (Ben and Chloe can too), and the key service never does; the hosted service hears it only because the agent sends it there.

Don’t send the key through the server

The key has to reach the agent by a path the forwarding server can’t read. LiveKit’s guide lists two. One passes the key in the job metadata, the data attached to the request that sends the agent into the room, and that request goes through the LiveKit server, which manages it, so the server sees the key. The other has the agent read the key from your own secrets store when it starts up, which keeps it away from the server.

Two things stay visible even with the inner lock:

  • Who speaks, when, and how loudly. The loudness byte is in an extension to the header, which neither lock encrypts (in SRTP, header extensions are “authenticated but not encrypted”), and SFrame’s own header carries a key ID and a counter that reveal the sender and the rate of their frames.
  • Anyone with the key can speak as anyone. SFrame shares one key per sender among all the ends, so “[a]ny sender in a session can send media that will be associated with any other sender” (§7.2). Giving the agent the key means trusting it, and whoever runs it, not to put words in Ana’s mouth.
The other names you'll meet: Encoded Transform, PERC, MLS

In a browser, the WebRTC Encoded Transform API is how a web app gets at each encoded frame to apply SFrame. PERC (RFC 8871), an earlier IETF design for private conferencing, allows a “Trusted Endpoint” holding the keys to be a mixer. And in designs like MLS the ends agree on keys among themselves, so no key service knows them.

What else the seat decides: noise suppression and recordings

Noise suppression often runs on each sender’s laptop before encoding, which works in any seat and with end-to-end encryption. But anything that needs everyone’s decoded audio in one place, like levelling speakers against each other or a mixed recording of the call, can’t run in a forwarding server. It has to land in the agent, in the mixer, or in another box that decodes, and on an end-to-end encrypted call that box becomes one of the ends.

Choosing a seat#

Start with one question about the model: does it need to know who said what? For a note-taker it does, and the mixer’s-client seat is out before you draw a box.

If this is truePut the agent
The model needs to know who said whatThrough a forwarding server, listening to every stream, or inside the mixer
The call must stay end-to-end encrypted from the serversThrough a forwarding server, holding the call’s key. Its operator and the key service’s become ends of the call, and so does any hosted model or speech service it sends audio to, which users should be told about
Everyone’s audio has to be processed together on the serverInside the mixer, next to the decoded audio
One person and one agentNot a mixer’s client: a mix of one person adds a decode and an encode for nothing. With one person, the agent’s server is simply the other end of the call, as in Where the server sits

Then check three things in your own call:

  1. Find the process that calls the decoder for the agent’s input. That’s your real seat, whatever the architecture diagram says.
  2. Run the counts for your real call size, python -m call_topology.counts --people N, to compare decodes per seat.
  3. List everyone who can hear what the agent hears, and check it against what your users were told:
    • the mixer’s operator, in every seat that has a mixer;
    • the forwarding server’s operator, unless the call is end-to-end encrypted and the key never passes through it;
    • whoever runs the agent and any key service;
    • every speech or model API the agent calls.

The demo#

The simulation is in participant-that-decodes/: a forwarding server that reads headers only, a mixer that decodes and sends everyone but themselves, an agent, a toy end-to-end cipher, and the counts. It needs only Python 3.10 or newer; the tests need pytest.

Shell
Terminal window
cd mainlog-code/participant-that-decodes
python -m call_topology.demo forward --speaker Chloe # renumbering through a forwarder
python -m call_topology.demo loss # a network loss stays a gap
python -m call_topology.demo mix # what a mixer decodes and sends
python -m call_topology.demo keys # who can open a sealed packet
python -m call_topology.counts --people 8 # decodes per seat
python -m pytest tests

Its “codec” is raw 16-bit samples, so decoding is just unpacking bytes. A real call uses Opus, which changes the cost of each decode but not where the decodes happen. Its CI runs the tests and checks a line from each demo quoted here.

So the note-taker’s seat decides two things at once: whether its model hears Ana, Ben and Chloe or one blend, and whether it has to be trusted with the call’s key. For a note-taker on an encrypted call, that’s one answer: through the forwarding server, decoding each person itself, holding the key as one of the ends, and with everyone it sends audio to on the list your users see.

Share this article

Related posts

3 items
  • Oct 19, 2026 21 min read AI

    Circuit breakers for inference

    Timeouts, retries and budgets for model calls that finish too slowly, produce unusable answers or keep repeating themselves. With the real Anthropic SDK, timeout=3.0 let an 18-second stream finish. In a simulated support chat, with invented failure rates, five checks took the conversations waiting over two minutes from 249 to 0.

    AI & agentic architecturesai-agentsreliability
  • Oct 26, 2026 20 min read AI

    What an agent should remember

    Agent memory stores mostly decide what to forget by time: a TTL, or deleting what hasn't been used. In a 90-day scripted scenario, that kept a cancelled booking alive for weeks and stored a restaurant's sentence as the user's preference. Two fields fix both: who wrote the memory, and which event would make it false.

    AI & agentic architecturesai-agentsarchitecture
  • Oct 9, 2026 29 min read AI

    What a state machine buys an agent

    The same dinner booking, sent to Claude as a free-form tool loop and as a state machine. Per-state tool lists sent 61% fewer tokens, but with prompt caching on they cost 23% more on input, and with output counted the two came out about even. The real saving was code running the steps that need no model, 8 calls down to 2 and about 72% off the total, plus a table of every state against every event.

    AI & agentic architecturesai-agentsprompt-engineeringarchitecture
  • AI

    Circuit breakers for inference

    Timeouts, retries and budgets for model calls that finish too slowly, produce unusable answers or keep repeating themselves. With the real Anthropic SDK, timeout=3.0 let an 18-second stream finish. In a simulated support chat, with invented failure rates, five checks took the conversations waiting over two minutes from 249 to 0.

    ai-agentsreliability
    Oct 2026
  • AI

    What an agent should remember

    Agent memory stores mostly decide what to forget by time: a TTL, or deleting what hasn't been used. In a 90-day scripted scenario, that kept a cancelled booking alive for weeks and stored a restaurant's sentence as the user's preference. Two fields fix both: who wrote the memory, and which event would make it false.

    ai-agentsarchitecture
    Oct 2026
  • AI

    What a state machine buys an agent

    The same dinner booking, sent to Claude as a free-form tool loop and as a state machine. Per-state tool lists sent 61% fewer tokens, but with prompt caching on they cost 23% more on input, and with output counted the two came out about even. The real saving was code running the steps that need no model, 8 calls down to 2 and about 72% off the total, plus a table of every state against every event.

    ai-agentsprompt-engineering
    Oct 2026

Comments