| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
`WrappedJoinRequest` already compresses the join payload carried in the connect URL, but every message after the handshake goes over the WebSocket uncompressed. On the resume path that is where the bulk of the bytes are: `SyncState` alone carries two full session descriptions plus the subscription, publish and data channel lists, and the publisher ICE-restart offer follows right behind it. Measured against a minimal one-audio/one-video offer, an SDP is 5734 bytes raw and 1515 gzipped -- a 3.8x saving on the single largest repeated payload. Signalling is one TCP connection, so these are not merely large, they are in the way: a multi-kilobyte offer in flight head-of-line blocks everything queued behind it, including the trickle candidates the PeerConnection needs to finish its ICE restart. Compressing them shortens recovery on exactly the degraded networks where recovery matters. `permessage-deflate` would have avoided a protocol change -- livekit-server already sets `EnableCompression: true` on the client-facing upgrader -- but no usable Rust WebSocket crate implements it, and it would not reach SDKs whose host application supplies its own WebSocket transport. Doing it at the application layer covers every transport uniformly, and mirrors what the SFU already does gzipping signal messages on its own internal transport. Negotiation reuses the existing `CAP_COMPRESSION_DEFLATE_RAW` capability, which clients already advertise and nothing currently reads. The server acknowledges it in `JoinResponse`/`ReconnectResponse`; unset means the current uncompressed format, so old and new peers on either side interoperate unchanged. Those two responses are themselves unwrapped, since they are what establish the agreement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 6febbf7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Sorry, something went wrong.
Replaces the top-level `SignalCompression` enum with a holder message carrying a nested `Type`. Both envelopes now reference `SignalCompression.Type`. The top-level form forced prefixed value names: enum values follow C++ scoping and are siblings of their type, so a bare `NONE` collides with the one already declared in livekit_sip.proto -- verified against protoc, which rejects it outright. Nesting scopes the values, so `NONE`/`GZIP`/`DEFLATE_RAW` read the same way as every other enum in this file. Nesting an enum inside each envelope, mirroring WrappedJoinRequest exactly, would also have avoided the prefix, but at the cost of two structurally identical yet type-incompatible enums. Compression is naturally generic over direction -- the SFU's own internal implementation threads a single compression type through both the request and response paths -- and splitting the type would force conversion code on every implementation for no gain. Value numbers match WrappedJoinRequest.Compression rather than ranking the options; two enums in one file where GZIP has different numbers invites mistakes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…flag Replaces `JoinResponse.signal_compression` and `ReconnectResponse.signal_compression` with a `SignalCompressionAck` arm on the `SignalResponse` oneof, sent unwrapped as the first message on the connection. The flags could not cover the message carrying them. A client has to know whether to parse the first frame as `SignalResponse` or `WrappedSignalResponse` before it can read anything out of it, so a flag inside the JoinResponse necessarily left that JoinResponse uncompressed -- and on a large room the join roster is the single biggest message on the wire, scaling with participant count and unbounded through participant metadata and attributes. Exactly the case worth compressing. A separate first frame removes the circularity without any sniffing. Both possibilities for frame one are a plain `SignalResponse`, so the client parses one type and switches on the arm it receives: `compression_ack` means compress from here on, anything else means an older server and the connection carries on uncompressed. A client that never advertised the capability is never sent the ack. All four old/new combinations keep working, and there is no extra round trip -- the server writes the ack and the JoinResponse back to back. Collapsing the two per-response booleans into one mechanism also drops the question of what a resume inherits: the ack is exchanged per connection, so a resume landing on a different node simply negotiates again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces WrappedSignalRequest/WrappedSignalResponse and the SignalCompressionAck handshake with a `compressed` arm on the SignalRequest and SignalResponse oneofs. The envelope form needed a negotiation handshake for one reason: a receiver had to know whether to parse a frame as SignalResponse or WrappedSignalResponse before it could read anything out of it. That forced an agreement to be established first, and whatever message established it could not itself be compressed. As an arm of the oneof, the tag is the discriminator. The receiver always parses a SignalResponse and switches on the arm it got, so nothing needs to be agreed in advance and the very first message can be compressed -- including JoinResponse, which in a large room is the biggest message on the wire and the one this is most worth doing for. That removes the extra frame, the ack message, and the two per-response booleans that preceded it. It is also cheaper on the wire. Under the envelope, once compression was on every message paid envelope overhead, including messages too small to be worth compressing and sent with compression NONE. Here a small message is sent as its ordinary arm and costs nothing, so the threshold has no downside. Gating is unchanged: the arm is used only against a peer that advertised CAP_COMPRESSION_DEFLATE_RAW, since an older peer would see an unknown field and silently drop the message. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…uests The capability gate only covered one direction. ClientInfo.CAP_COMPRESSION_DEFLATE_RAW tells the server what the client can decode, so server-to-server compression was safe. Nothing told the client whether the server could decode a compressed SignalRequest. Guessing wrong fails in the worst possible way. An older server parses the unknown field into its unknown-field set and leaves the oneof unset; the switch in livekit-server's signalhandler has no default case, so the message is dropped with no error and no warning. A resume would stall on a SyncState that never arrived rather than failing and escalating. Adds accepts_compressed_signal to JoinResponse and ReconnectResponse. This is not the circular arrangement removed earlier: that flag had to be read before the client could parse the message carrying it, whereas this one gates only what the client SENDS. Parsing is still decided by the oneof tag alone, so the JoinResponse carrying the flag can itself be compressed. ReconnectResponse repeats it because a resume may land on a different node, and because the largest client-to-server message of all -- SyncState, carrying two session descriptions plus the subscription and publish lists -- is sent immediately after it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| Back | FazBrowse Home | New Git URL |
Proposal
Let signal messages on the WebSocket be compressed, the way WrappedJoinRequest already compresses the join payload in the connect URL. Adds a compressed arm to the SignalRequest / SignalResponse oneofs, carrying CompressedSignalRequest / CompressedSignalResponse and a shared SignalCompression.Type enum.
No negotiation handshake, no extra frames, no version gate. The receiver always parses a SignalRequest/SignalResponse; the oneof tag says whether the payload is compressed.
Draft — the proto compiles, but I'd like agreement on the shape before anyone implements against it.
Why
Everything after the handshake goes over the socket uncompressed. On the resume path that's where the bulk of the bytes are:
Measured against a real minimal one-audio/one-video offer: 5,734 bytes raw → 1,515 gzipped (3.8×).
Size isn't the only cost. Signalling is a single TCP connection, so a multi-kilobyte offer in flight head-of-line blocks everything queued behind it — including the trickle candidates the PeerConnection needs to finish its ICE restart. Compressing these directly shortens recovery on the degraded networks where recovery matters most.
How it works
Receiver parses SignalResponse. If it lands on compressed, it inflates signal_response and parses that as a SignalResponse. One level only; senders must not nest.
Senders use the arm only against a peer that advertised ClientInfo.CAP_COMPRESSION_DEFLATE_RAW — which clients already send today and nothing currently reads in livekit-server or cloud. An older peer would see an unknown field and silently drop the message, so the capability gate is load-bearing.
Below roughly 200 bytes, senders just use the ordinary arm — matching the threshold cloud/pkg/rtc/signal/compress.go:14 already uses internally.
Why this shape
Two earlier revisions in this branch, both worse:
As a oneof arm, the tag is the discriminator, so neither is needed. It's also cheaper on the wire: under an envelope, every message pays envelope overhead once compression is on, including small ones sent with compression = NONE. Here a small message is its ordinary arm and costs nothing, so the threshold has no downside.
Why not permessage-deflate
It would need no protocol change, and livekit-server already sets EnableCompression: true on the client-facing upgrader (pkg/service/rtcservice.go:76-77). But:
Application-layer compression covers every transport uniformly, and mirrors what the SFU already does gzipping SignalRequest/SignalResponse on its own node-to-node transport.
Shape of the enum
SignalCompression is a holder message with a nested Type, rather than a top-level enum or an enum nested in each message:
Values are numbered to match WrappedJoinRequest.Compression rather than to rank the options; two enums in one file where GZIP has different numbers invites mistakes.
Open questions
Notes
🤖 Generated with Claude Code