Matrix: calls are one-to-one only #122

Closed
opened 2026-09-06 12:04:31 -04:00 by Salastil · 1 comment
Owner

#81 landed voice and video over m.call.*, which is Matrix's original one-to-one
signalling: one offer, one answer, one other person. A room with three people in it
has no call in moho - there is no m.call.notify, no MSC3401 group session, and
nothing in the daemon that mentions either.

The window is ready for it. The video surface already draws a fitted grid of any
number of tiles, lights whoever is talking, and lets one person be enlarged
(components/CallStage.tsx, VideoStage.tsx) - that work was done against a call of
five and only ever fed by two. What is missing is the signalling underneath.

What is needed

  • MSC3401 group call membership (m.call.member state) and m.call.notify for the
    ring, or Element Call's current shape if that has settled by the time this is
    picked up - it should be read again before anything is written, since this part of
    Matrix moves.
  • A peer connection per participant in the renderer, which is what Call
    (lib/webrtc.ts) would need generalising to hold.
  • Leave and rejoin without ending it for everybody else, which is the difference
    between a group call and a conference somebody hosts.

Done looks like: three people in a room can be in the same call, and the grid that
already exists draws them.

Reference: Element. Follows #81.

#81 landed voice and video over `m.call.*`, which is Matrix's original one-to-one signalling: one offer, one answer, one other person. A room with three people in it has no call in moho - there is no `m.call.notify`, no MSC3401 group session, and nothing in the daemon that mentions either. The window is ready for it. The video surface already draws a fitted grid of any number of tiles, lights whoever is talking, and lets one person be enlarged (`components/CallStage.tsx`, `VideoStage.tsx`) - that work was done against a call of five and only ever fed by two. What is missing is the signalling underneath. **What is needed** - MSC3401 group call membership (`m.call.member` state) and `m.call.notify` for the ring, or Element Call's current shape if that has settled by the time this is picked up - it should be read again before anything is written, since this part of Matrix moves. - A peer connection per participant in the renderer, which is what `Call` (`lib/webrtc.ts`) would need generalising to hold. - Leave and rejoin without ending it for everybody else, which is the difference between a group call and a conference somebody hosts. **Done looks like:** three people in a room can be in the same call, and the grid that already exists draws them. Reference: Element. Follows #81.
Salastil added the featureprotocol labels 2026-09-06 12:04:31 -04:00
Salastil removed the protocol label 2026-09-06 12:49:15 -04:00
Author
Owner

Finished — https://git.salastil.com/Salastil/nobilis/commit/6fd10f7 and https://git.salastil.com/Salastil/moho/commit/ce64d76

A call in a room, between everybody in it: m.call.member state says who is in one, and the media is a mesh of one connection per pair.

  • Joining is starting. Publishing this end's membership is what makes a call exist; the difference is only whether anybody was there first. Memberships carry an expiry and are refreshed while still here, so a client that dies mid-call stops being a participant.
  • Who calls whom is a property of the pair, not of who arrived first — the smaller key calls the larger, computed identically on both sides. Otherwise both ends call and the call collides with itself.
  • One capture for the whole call, lent to each leg: five connections asking for the same camera get one capture and four refusals.
  • A bar at the top of a room where a call is happening, because a group call has no invitation. The header buttons start a room call in a channel and ring somebody in a DM.
  • The permission trap, which would otherwise fail silently: a membership is a state event, and a room's default lets only moderators send those — so a call in an ordinary room is one nobody else can join. Starting a call now lowers m.call.member to power 0 where this account may say so, and the refusal explains itself where it may not. This was found by hitting it: the second account's join came back M_FORBIDDEN: Event is not authorized.

Verified end to end between two real accounts on two different homeservers — @salastil:poa.st and @salastil:matrix.salastil.com, in a room created for the test:

A joins            → m.call.member published; B's own view of the room reports it
B's window         → "‎@salastil:poa.st is in a call — Join / Join with video"
B joins            → both ends: "Connected", 2 tiles each, live audio track on both
B hangs up         → B's stage gone, bar back to offering Join; A back to 1 tile

Audio only, because this machine has no camera — the fallback path did its job.

Two things worth knowing. This is moho-to-moho: Element Call now runs through an SFU (LiveKit) rather than a mesh, so it will not join one of these — full interop needs the focus half of MSC3401 and is a separate piece of work. And a mesh costs one upload stream per other participant, which is fine for the handful of people a room call usually is and would not be fine for thirty.

Finished — https://git.salastil.com/Salastil/nobilis/commit/6fd10f7 and https://git.salastil.com/Salastil/moho/commit/ce64d76 A call in a room, between everybody in it: `m.call.member` state says who is in one, and the media is a mesh of one connection per pair. - **Joining is starting.** Publishing this end's membership is what makes a call exist; the difference is only whether anybody was there first. Memberships carry an expiry and are refreshed while still here, so a client that dies mid-call stops being a participant. - **Who calls whom** is a property of the pair, not of who arrived first — the smaller key calls the larger, computed identically on both sides. Otherwise both ends call and the call collides with itself. - **One capture for the whole call**, lent to each leg: five connections asking for the same camera get one capture and four refusals. - **A bar at the top of a room** where a call is happening, because a group call has no invitation. The header buttons start a room call in a channel and ring somebody in a DM. - **The permission trap**, which would otherwise fail silently: a membership is a *state* event, and a room's default lets only moderators send those — so a call in an ordinary room is one nobody else can join. Starting a call now lowers `m.call.member` to power 0 where this account may say so, and the refusal explains itself where it may not. This was found by hitting it: the second account's join came back `M_FORBIDDEN: Event is not authorized`. **Verified end to end between two real accounts on two different homeservers** — `@salastil:poa.st` and `@salastil:matrix.salastil.com`, in a room created for the test: ``` A joins → m.call.member published; B's own view of the room reports it B's window → "‎@salastil:poa.st is in a call — Join / Join with video" B joins → both ends: "Connected", 2 tiles each, live audio track on both B hangs up → B's stage gone, bar back to offering Join; A back to 1 tile ``` Audio only, because this machine has no camera — the fallback path did its job. **Two things worth knowing.** This is moho-to-moho: Element Call now runs through an SFU (LiveKit) rather than a mesh, so it will not join one of these — full interop needs the focus half of MSC3401 and is a separate piece of work. And a mesh costs one upload stream per other participant, which is fine for the handful of people a room call usually is and would not be fine for thirty.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Salastil/moho#122