mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 04:55:42 +09:00
feat(voice): add opt-in peer-to-peer calls and country rollouts (#3367)
This commit is contained in:
1 parent
e5a568acca
commit
bef5e33eec
215 files changed
+15840
-2693
No files matched your search
@@ -290,7 +290,13 @@ export default defineConfig({
|
||||
},
|
||||
{
|
||||
label: 'Voice',
|
||||
items: ['voice', 'http-api/calls', 'http-api/streams', 'http-api/entrance-sounds'],
|
||||
items: [
|
||||
'voice',
|
||||
'http-api/calls',
|
||||
'http-api/peer-to-peer-voice',
|
||||
'http-api/streams',
|
||||
'http-api/entrance-sounds',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Admin API',
|
||||
|
||||
@@ -36,6 +36,7 @@ Missing settings use the defaults documented below. Invalid stored configuration
|
||||
| gateway_rollout | [Gateway rollout configuration](#gateway-rollout-configuration-object) object | Gateway admission and dispatch tuning |
|
||||
| push_relay | [push relay configuration](#push-relay-configuration-object) object | Operator consent to the Fluxer-run push relay |
|
||||
| domain_migration | [domain migration configuration](#domain-migration-configuration-object) object | Web domain migration rollout |
|
||||
| voice_p2p | [peer-to-peer voice configuration](#peer-to-peer-voice-configuration-object) object | Peer-to-peer voice rollout |
|
||||
| captcha | [captcha configuration](#captcha-configuration-object) object | ALTCHA proof-of-work captcha settings |
|
||||
| experiment_delivery | [experiment delivery configuration](#experiment-delivery-configuration-object) object | Cadence every client polls the experiments route on |
|
||||
| registration | [registration configuration](#registration-configuration-object) object | Registration policy, issued URLs, and pending registrations |
|
||||
@@ -139,6 +140,7 @@ The instance rollout that moves the official web client from its legacy origin t
|
||||
| enabled | boolean | Whether the rollout runs at all (default false) |
|
||||
| config_version | integer | Revision counter, raised by Fluxer and never accepted from a request |
|
||||
| rollout_basis_points | integer | Share of accounts the rollout selects, in basis points (0-10000, default 0) |
|
||||
| rollout_country_codes | array[string] | Countries the sampled share is limited to, as unique uppercase ISO 3166-1 alpha-2 codes, up to 250 (default empty) |
|
||||
| rollout_salt | string | Salt of the sampling hash (1-64 printable ASCII characters, default `domain-migration-v1`) |
|
||||
| included_user_ids | array[snowflake] | Accounts the rollout always selects, up to 1000 entries (default empty) |
|
||||
| included_guild_ids | array[snowflake] | Guilds whose members the rollout always selects, up to 1000 entries (default empty) |
|
||||
@@ -151,6 +153,8 @@ Every field is present on read. An absent document or missing field uses the def
|
||||
|
||||
`excluded_user_ids` wins over every other rule. Otherwise the rollout selects an account in `included_user_ids`, a member of a guild in `included_guild_ids`, or a premium account while `include_premium_users` is true, whatever `rollout_basis_points` says.
|
||||
|
||||
While `rollout_country_codes` is empty, `rollout_basis_points` samples every account. Once it lists a country, the sampled share only selects an account whose request comes from a listed country, and a request with an unknown country is never sampled. The list does not affect `excluded_user_ids`, `included_user_ids`, `included_guild_ids`, `include_premium_users`, or `anonymous_rollout_basis_points`.
|
||||
|
||||
An installed Chromium desktop web app moves its data like a browser tab, then stays on the legacy origin and offers to install the app from the new one. Set `standalone_forwarding` once the web app manifest lists the new origin in `scope_extensions` and the new origin serves the matching association file. From then on the installed app forwards like a browser tab. Installed mobile and Safari web apps never forward, whatever the value.
|
||||
|
||||
Only the official web client acts on this configuration. On any other instance it changes nothing a client does.
|
||||
@@ -159,6 +163,39 @@ Only the official web client acts on this configuration. On any other instance i
|
||||
Setting `enabled` to false stops new migrations and also stops forwarding from the legacy origin for clients that already moved. Excluding an account only stops a migration that has not started yet.
|
||||
:::
|
||||
|
||||
## Peer-to-peer voice configuration object
|
||||
|
||||
The instance rollout that lets small voice calls connect their participants directly instead of through the voice servers. [Experiments](/http-api/experiments/#peer-to-peer-voice-assignment-object) defines what a client resolves from it.
|
||||
|
||||
### Structure
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| enabled | boolean | Whether the rollout runs at all (default false) |
|
||||
| config_version | integer | Revision counter, raised by Fluxer and never accepted from a request |
|
||||
| rollout_basis_points | integer | Share of accounts the rollout selects, in basis points (0-10000, default 0) |
|
||||
| rollout_country_codes | array[string] | Countries the sampled share is limited to, as unique uppercase ISO 3166-1 alpha-2 codes, up to 250 (default empty) |
|
||||
| rollout_salt | string | Salt of the sampling hash (1-64 printable ASCII characters, default `voice-p2p-v1`) |
|
||||
| included_user_ids | array[snowflake] | Accounts the rollout always selects, up to 1000 entries (default empty) |
|
||||
| included_guild_ids | array[snowflake] | Guilds whose members the rollout always selects, up to 1000 entries (default empty) |
|
||||
| include_premium_users | boolean | Whether the rollout always selects accounts with active premium (default false) |
|
||||
| excluded_user_ids | array[snowflake] | Accounts the rollout never selects, up to 1000 entries (default empty) |
|
||||
| max_participants | integer | Most participants a peer-to-peer call holds (2-4, default 2) |
|
||||
|
||||
Every field is present on read. An absent document or missing field uses the defaults above.
|
||||
|
||||
`excluded_user_ids` wins over every other rule. Otherwise the rollout selects an account in `included_user_ids`, a member of a guild in `included_guild_ids`, or a premium account while `include_premium_users` is true, whatever `rollout_basis_points` says.
|
||||
|
||||
While `rollout_country_codes` is empty, `rollout_basis_points` samples every account. Once it lists a country, the sampled share only selects an account whose request comes from a listed country, and a request with an unknown country is never sampled. The list does not affect `excluded_user_ids`, `included_user_ids`, `included_guild_ids`, or `include_premium_users`.
|
||||
|
||||
The rules above select who may start a peer-to-peer call. Joining a call that already runs peer-to-peer needs only `enabled`, so a partial rollout does not keep unselected accounts out of running calls.
|
||||
|
||||
`max_participants` applies to every account, selected or not. A join that would take a peer-to-peer call past it is refused as full, as [Peer-to-peer voice](/gateway/events/#peer-to-peer-voice) describes. Lowering it removes nobody from a running call.
|
||||
|
||||
:::caution[`enabled` is the kill switch]
|
||||
Setting `enabled` to false makes every new call use the voice servers. A call that already runs peer-to-peer keeps running and refuses new joins until it ends or a participant switches it to the voice servers. Fluxer never moves a running call on its own.
|
||||
:::
|
||||
|
||||
## Captcha configuration object
|
||||
|
||||
The ALTCHA proof-of-work check that the API issues and verifies itself on gated operations. [CAPTCHA handling](/topics/captcha/) defines the challenge exchange and lists the gated operations.
|
||||
@@ -611,6 +648,7 @@ The body has one optional object for each section. Fluxer leaves an absent secti
|
||||
| gateway_rollout? | object | Any subset of the [Gateway rollout configuration](#gateway-rollout-configuration-object) fields, each bound as documented there |
|
||||
| push_relay? | object | `relay_consent_accepted` from the [push relay configuration](#push-relay-configuration-object) |
|
||||
| domain_migration? | object | Any subset of the [domain migration](#domain-migration-configuration-object) fields |
|
||||
| voice_p2p? | object | Any subset of the [peer-to-peer voice](#peer-to-peer-voice-configuration-object) fields |
|
||||
| captcha? | object | Any subset of the [captcha configuration](#captcha-configuration-object) fields |
|
||||
| experiment_delivery? | object | Any subset of the [experiment delivery](#experiment-delivery-configuration-object) fields |
|
||||
| registration? | object | `mode` and `admin_registration_urls_enabled` |
|
||||
@@ -628,6 +666,8 @@ The body has one optional object for each section. Fluxer leaves an absent secti
|
||||
|
||||
`domain_migration` takes every [domain migration configuration](#domain-migration-configuration-object) field except `config_version`, each bound as documented there. Fluxer raises `config_version` by one on each request that supplies at least one of them. A section that is absent, or present with no field set, writes nothing and leaves `config_version` alone.
|
||||
|
||||
`voice_p2p` works the same way, over the [peer-to-peer voice configuration](#peer-to-peer-voice-configuration-object) fields and its own `config_version`.
|
||||
|
||||
`captcha` takes any subset of the [captcha configuration](#captcha-configuration-object) fields, each bound as documented there. Fluxer merges the supplied fields over the stored ones. The section has no `config_version`.
|
||||
|
||||
`experiment_delivery` takes both [experiment delivery configuration](#experiment-delivery-configuration-object) fields, each bound as documented there. It is a section of its own, so a write to it changes no `config_version` and changes no assignment, only the cadence on which clients ask for one.
|
||||
@@ -668,7 +708,7 @@ On a self-hosted deployment, billing and the `everyone` premium mode exclude eac
|
||||
| 400 | [error response](/admin-api/#error-response) | A policy transition is refused, returned as `INSTANCE_POLICY_TRANSITION_NOT_ALLOWED` |
|
||||
|
||||
:::caution[Sections are applied one after another]
|
||||
The order is `gateway_rollout`, `push_relay`, `domain_migration`, `captcha`, `experiment_delivery`, `sso`, `registration`, `app_public` branding, legal, and registration fields, `integrations`, `media`, `policy`, `billing`, and finally `app_public.setup`. A failure part way through leaves the earlier sections written.
|
||||
The order is `gateway_rollout`, `push_relay`, `domain_migration`, `voice_p2p`, `captcha`, `experiment_delivery`, `sso`, `registration`, `app_public` branding, legal, and registration fields, `integrations`, `media`, `policy`, `billing`, and finally `app_public.setup`. A failure part way through leaves the earlier sections written.
|
||||
:::
|
||||
|
||||
### Side effects
|
||||
|
||||
@@ -15,8 +15,9 @@ Except for [Heartbeat](#heartbeat), [Identify](#identify), and [Resume](#resume)
|
||||
| 1 | Heartbeat | Opcode `11` Heartbeat ACK |
|
||||
| 2 | Identify | [Ready](/gateway/events/#ready), a close frame, or no response when Fluxer discards a rate-limited Identify or holds it to retry the session start |
|
||||
| 3 | Presence Update | No direct response |
|
||||
| 4 | Voice State Update | [Voice State Update](/gateway/events/#voice-state-update) and [Voice Server Update](/gateway/events/#voice-server-update) when state changes |
|
||||
| 4 | Voice State Update | [Voice State Update](/gateway/events/#voice-state-update) and [Voice Server Update](/gateway/events/#voice-server-update) when state changes, or [Gateway Error](/gateway/opcodes-and-close-codes/#gateway-error) for a reported refusal |
|
||||
| 6 | Resume | Replayed Dispatches followed by [Resumed](/gateway/events/#resumed), Invalid Session, or a close frame |
|
||||
| 13 | Voice Signal | [Voice Signal](/gateway/events/#voice-signal) for the target connection's session, or no response |
|
||||
| 8 | Request Guild Members | One or more [Guild Members Chunk](/gateway/events/#guild-members-chunk) events |
|
||||
| 14 | Lazy Request | [Guild Sync](/gateway/events/#guild-sync), [Guild Member List Update](/gateway/events/#guild-member-list-update), [Thread List Sync](/gateway/threads/#thread-list-sync), and [Thread Member List Update](/gateway/threads/#thread-member-list-update) |
|
||||
| 15 | Request Guild Counts | [Guild Counts Update](/gateway/events/#guild-counts-update) |
|
||||
@@ -267,6 +268,7 @@ Opcode `4` joins, moves, updates, or leaves the voice membership associated with
|
||||
| viewer_stream_keys?<sup>3</sup> | ?array[string] | The stream keys this connection is watching, where an omitted key keeps the current list and null clears it |
|
||||
| latitude? | number or string | The client latitude, used to pick a voice region |
|
||||
| longitude? | number or string | The client longitude, used to pick a voice region |
|
||||
| p2p?<sup>5</sup> | boolean | Whether the client agrees to peer-to-peer media for a new connection, or leaves the mesh on an existing one |
|
||||
|
||||
<sup>1</sup> A `channel_id` with no `connection_id` opens a new connection, and a `channel_id` with one updates or moves that connection. An update that leaves one guild, meaning a non-null `guild_id` with `channel_id: null`, requires a `connection_id`, and one that omits it is refused with `VOICE_MISSING_CONNECTION_ID`
|
||||
|
||||
@@ -276,6 +278,8 @@ Opcode `4` joins, moves, updates, or leaves the voice membership associated with
|
||||
|
||||
<sup>4</sup> Only `true` and the string `"true"` set it, and Fluxer publishes `false` when the member lacks `STREAM` in the channel. The screenshare track uses this same connection, so setting the flag issues no grant and sends no [Voice Server Update](/gateway/events/#voice-server-update)
|
||||
|
||||
<sup>5</sup> Only the JSON Boolean `true` agrees and only the JSON Boolean `false` switches. On a new connection `true` states that the client supports peer-to-peer media and accepts it for this join. A bot session never agrees, whatever it sends. On an existing connection an explicit `false` converts the whole channel or call to LiveKit, and an omitted key or `true` changes nothing. [Peer-to-peer voice](/gateway/events/#peer-to-peer-voice) states when a join is peer-to-peer and when it is refused
|
||||
|
||||
```json
|
||||
{
|
||||
"op": 4,
|
||||
@@ -298,10 +302,46 @@ Every field is optional. A non-null `guild_id` or `channel_id` is a canonical de
|
||||
|
||||
The command has no `session_id` field. Fluxer identifies the voice membership by the Gateway session that sends the command.
|
||||
|
||||
Joining or replacing a grant produces [Voice Server Update](/gateway/events/#voice-server-update) with the token and endpoint for the media connection, and [Voice State Update](/gateway/events/#voice-state-update) for every session that can see the channel.
|
||||
Joining or replacing a grant produces [Voice Server Update](/gateway/events/#voice-server-update) with the token and endpoint for the media connection, or with the ICE servers of a peer-to-peer grant, and [Voice State Update](/gateway/events/#voice-state-update) for every session that can see the channel.
|
||||
|
||||
A refusal with `VOICE_P2P_CONSENT_REQUIRED`, `VOICE_P2P_UNAVAILABLE`, or `VOICE_CHANNEL_FULL` reaches the requesting session as a [Gateway Error](/gateway/opcodes-and-close-codes/#gateway-error) payload. Every other refusal sends nothing.
|
||||
|
||||
The first two updates in a rolling one-second window take effect immediately. Later updates can be delayed or replaced by newer updates for the same `guild_id` and `connection_id`. See [command rate limits](/gateway/limits-and-rate-limits/#connection-and-command-rate-limits) for the timing and capacity bounds.
|
||||
|
||||
## Voice Signal
|
||||
|
||||
Opcode `13` sends one signalling message to one other connection of a [peer-to-peer](/gateway/events/#peer-to-peer-voice) voice channel or call.
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| guild_id? | ?snowflake | The guild containing the voice channel, where null or an omitted key selects the DM and group DM call context |
|
||||
| channel_id | snowflake | The voice channel or call channel both connections are in |
|
||||
| to | string | The `connection_id` of the voice state the signal is for |
|
||||
| data | object | The signalling message, which Fluxer relays unread |
|
||||
|
||||
```json
|
||||
{
|
||||
"op": 13,
|
||||
"d": {
|
||||
"guild_id": "1189375284394692608",
|
||||
"channel_id": "1189375284394692610",
|
||||
"to": "b7f3c2a94e1d4c0f",
|
||||
"data": {
|
||||
"type": "offer",
|
||||
"sdp": "v=0..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Fluxer relays the signal when the sending session owns a voice state with `p2p: true` in that channel and `to` names another voice state with `p2p: true` in the same channel. The target connection's session alone receives [Voice Signal](/gateway/events/#voice-signal).
|
||||
|
||||
Every other signal is dropped with no event and no close. That covers a channel or call that is not peer-to-peer, a `to` that names no connection there, a `to` that names the sender's own connection, a `data` that is not an object, and a session with no voice state in the channel.
|
||||
|
||||
`data` is opaque to Fluxer. Clients agree on its contents among themselves, typically a session description or an ICE candidate. The whole frame stays within the [inbound payload bound](/gateway/overview/#framing).
|
||||
|
||||
A client sends this command only after it has received a [Voice Server Update](/gateway/events/#voice-server-update) with `p2p: true` for the connection. [Command rate limits](/gateway/limits-and-rate-limits/#connection-and-command-rate-limits) states the Voice Signal budget.
|
||||
|
||||
## Request Guild Members
|
||||
|
||||
Opcode `8` requests bounded member chunks.
|
||||
|
||||
@@ -114,6 +114,7 @@ A Dispatch is buffered for [Resume](/gateway/commands/#resume) replay unless it
|
||||
| [Channel Pins ACK](#channel-pins-ack) | The current user acknowledges a channel's pins | Current user |
|
||||
| [Voice State Update](#voice-state-update) | A guild or call participant's voice state changes | Channel visibility |
|
||||
| [Voice Server Update](#voice-server-update) | The session receives or replaces its own voice grant | Current session |
|
||||
| [Voice Signal](#voice-signal) | Another peer-to-peer voice connection sends this connection a signalling message | Current session |
|
||||
| [Entrance Sound Play](#entrance-sound-play) | A participant's entrance sound plays in a voice channel | Voice channel |
|
||||
| [Call Create](#call-create) | A private channel call begins or becomes visible | Call recipient |
|
||||
| [Call Update](#call-update) | The ringing set, participant roster, or region of a call changes | Call recipient |
|
||||
@@ -985,6 +986,7 @@ A `channel_id` of null means the participant left.
|
||||
| suppress | boolean | Whether the participant is suppressed |
|
||||
| viewer_stream_keys | array[string] | Streams this connection is watching |
|
||||
| e2ee_capable | boolean | Whether the participant's client supports end-to-end encrypted voice |
|
||||
| p2p | boolean | Whether this connection exchanges media peer-to-peer and not through LiveKit |
|
||||
| version | integer | Monotonic version of this participant's voice state |
|
||||
|
||||
<sup>1</sup> The participant's client reports this value. In a guild voice channel Fluxer sets it to false when the participant lacks `STREAM`
|
||||
@@ -995,23 +997,122 @@ The broadcast form has no `region_id`, `server_id`, `latitude`, or `longitude`.
|
||||
|
||||
The session received or replaced its own voice grant. Delivered to the requesting session alone.
|
||||
|
||||
A grant has one of two structures. A LiveKit grant has `token` and `endpoint`. A peer-to-peer grant has `p2p: true` and `ice_servers`, and never has `token`, `endpoint`, or `e2ee_key`.
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| token | string | The LiveKit access token this connection presents |
|
||||
| endpoint | string | The LiveKit signalling URL to connect to, a `ws://` or `wss://` address |
|
||||
| token?<sup>1</sup> | string | The LiveKit access token this connection presents |
|
||||
| endpoint?<sup>1</sup> | string | The LiveKit signalling URL to connect to, a `ws://` or `wss://` address |
|
||||
| p2p?<sup>2</sup> | boolean | Always true when present. The connection exchanges media peer-to-peer |
|
||||
| ice_servers?<sup>2</sup> | array[[ICE server object](#ice-server-object)] | The STUN servers the connection uses for its peer connections, empty when the instance names none |
|
||||
| connection_id | string | The voice connection the grant covers |
|
||||
| channel_id | snowflake | The channel the grant covers |
|
||||
| guild_id?<sup>1</sup> | snowflake | The guild the channel belongs to |
|
||||
| e2ee_key?<sup>2</sup> | string | The key material the channel's end-to-end encryption uses |
|
||||
| guild_id?<sup>3</sup> | snowflake | The guild the channel belongs to |
|
||||
| e2ee_key?<sup>4</sup> | string | The key material the channel's end-to-end encryption uses |
|
||||
|
||||
<sup>1</sup> Present for a guild voice channel and absent for a call, so a client reads the scope from this field
|
||||
<sup>1</sup> Present on a LiveKit grant
|
||||
|
||||
<sup>2</sup> Present only when the channel is end-to-end encrypted
|
||||
<sup>2</sup> Present on a peer-to-peer grant, which only a session that sent `p2p: true` in its [Voice State Update](/gateway/commands/#voice-state-update) command receives
|
||||
|
||||
Fluxer uses LiveKit for voice media. There is no second voice websocket, no voice opcode set, and no UDP discovery step. A client opens a LiveKit connection to `endpoint`, presents `token` there, and speaks the LiveKit protocol from that point on. [Voice](/voice/) states the room naming, the participant identity, and the track sources a grant admits.
|
||||
<sup>3</sup> Present for a guild voice channel and absent for a call, so a client reads the scope from this field
|
||||
|
||||
<sup>4</sup> Present only on a LiveKit grant for an end-to-end encrypted channel
|
||||
|
||||
#### ICE server object
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| urls | array[string] | The `stun:` URLs of one server |
|
||||
|
||||
Fluxer never hands out a TURN server, so a peer-to-peer connection never relays media. Peers that cannot reach each other directly fail to connect, and [Peer-to-peer voice](#peer-to-peer-voice) states what a client does then.
|
||||
|
||||
```json
|
||||
{
|
||||
"p2p": true,
|
||||
"ice_servers": [
|
||||
{"urls": ["stun:stun.example.com:3478"]}
|
||||
],
|
||||
"connection_id": "b7f3c2a94e1d4c0f",
|
||||
"channel_id": "1189375284394692610",
|
||||
"guild_id": "1189375284394692608"
|
||||
}
|
||||
```
|
||||
|
||||
Fluxer uses LiveKit for voice media unless the grant is peer-to-peer. There is no second voice websocket and no UDP discovery step. With a LiveKit grant a client opens a LiveKit connection to `endpoint`, presents `token` there, and speaks the LiveKit protocol from that point on. With a peer-to-peer grant a client follows [Peer-to-peer voice](#peer-to-peer-voice). [Voice](/voice/) states the room naming, the participant identity, and the track sources a grant admits.
|
||||
|
||||
A grant is issued when a connection opens, when it moves to another channel, and when its region changes. Toggling `self_mute`, `self_video`, or `self_stream` produces no new grant. A call region change reissues one grant to each participant of the call.
|
||||
|
||||
A LiveKit grant for a `connection_id` that holds a peer-to-peer grant means a participant or a region change converted the channel or call. The client closes its peer connections and connects to LiveKit with the new grant.
|
||||
|
||||
#### Peer-to-peer voice
|
||||
|
||||
A voice channel or call is peer-to-peer when it has at least one voice state and every voice state in it has `p2p: true`. The participants then form a full mesh of WebRTC peer connections, one for each pair of connections, and no media passes through LiveKit. The mesh roster is the set of voice states of the channel or call.
|
||||
|
||||
Fluxer decides each new connection as follows.
|
||||
|
||||
| Channel or call | The command sent `p2p: true` | Any other `p2p` value |
|
||||
| --- | --- | --- |
|
||||
| Peer-to-peer below the participant cap | Peer-to-peer join<sup>1</sup> | Refused with `VOICE_P2P_CONSENT_REQUIRED` |
|
||||
| Peer-to-peer at the participant cap | Refused with `VOICE_CHANNEL_FULL` | Refused with `VOICE_P2P_CONSENT_REQUIRED` |
|
||||
| Empty, with no join awaiting LiveKit confirmation | Peer-to-peer join, or a LiveKit join when Fluxer declines<sup>2</sup> | LiveKit join |
|
||||
| Every other state | LiveKit join | LiveKit join |
|
||||
|
||||
<sup>1</sup> Refused with `VOICE_P2P_UNAVAILABLE` when Fluxer declines peer-to-peer for this join
|
||||
|
||||
<sup>2</sup> The session then receives a LiveKit grant and the channel or call is not peer-to-peer
|
||||
|
||||
Participants are counted as connections in a guild voice channel and as users in a call. The participant cap is `max_participants` of the [peer-to-peer voice configuration](/admin-api/instance/#peer-to-peer-voice-configuration-object), 2 through 4, and every account reads it from its [peer-to-peer voice assignment](/http-api/experiments/#peer-to-peer-voice-assignment-object). A lower cap never removes a participant from a running call. A bot session counts as not having sent `p2p: true`. A connection that moves into a guild voice channel with its own Voice State Update command is decided by the same table.
|
||||
|
||||
Each refusal reaches the requesting session as a [Gateway Error](/gateway/opcodes-and-close-codes/#gateway-error) payload, changes no voice state, and leaves the existing participants untouched. A client that receives `VOICE_P2P_CONSENT_REQUIRED` asks its user and, on agreement, sends the join again with `p2p: true`.
|
||||
|
||||
Two joins can reach an empty call at the same moment with different `p2p` values. Fluxer admits the first and refuses the other with `VOICE_P2P_CONSENT_REQUIRED`. The refused session receives a [Voice State Update](#voice-state-update) with a null `channel_id` for the connection it had been granted.
|
||||
|
||||
The voice state of a peer-to-peer join is published at once, with no LiveKit confirmation step. Peers exchange session descriptions and ICE candidates with the [Voice Signal](/gateway/commands/#voice-signal) command, which arrives as [Voice Signal](#voice-signal).
|
||||
|
||||
Fluxer never converts a peer-to-peer channel or call to LiveKit by itself. Only two actions convert one.
|
||||
|
||||
- A participant sends a Voice State Update command for its own connection with an explicit `p2p: false`.
|
||||
- The voice region changes. [Modify call region](/http-api/calls/#modify-call-region) converts a call whenever it supplies `region`, even with the current value. [Modify channel](/http-api/channels/#modify-channel) converts a guild voice channel when it stores a different `rtc_region`.
|
||||
|
||||
A conversion sets `p2p` to false on every voice state of the channel or call, publishes each as a [Voice State Update](#voice-state-update), and sends each connected session a LiveKit grant for its existing `connection_id`. The conversion is one way. The channel or call can be peer-to-peer again only after it has emptied.
|
||||
|
||||
A guild voice channel whose `rtc_p2p` is true stays peer-to-peer. Neither action converts it, and a change to `rtc_p2p` takes effect for the next call, after the channel has emptied. Fluxer does not ask for agreement before the first join of such a channel. A client whose [peer-to-peer voice assignment](/http-api/experiments/#peer-to-peer-voice-assignment-object) is enabled asks its user and sends `p2p: true` on agreement. Any other first join starts a LiveKit call.
|
||||
|
||||
A failed peer connection converts nothing. The affected client reports the problem to its user, who can leave or send the explicit `p2p: false`. Each client reports how its peer connections settled with [Report connection outcomes](/http-api/peer-to-peer-voice/#report-connection-outcomes).
|
||||
|
||||
A moderator cannot move a member into a peer-to-peer channel. [Modify guild member](/http-api/guild-members/#modify-guild-member) returns 400 `USER_NOT_IN_VOICE` and the member stays in its channel. A member moved out of a peer-to-peer channel receives a LiveKit grant for the target channel.
|
||||
|
||||
Server mute and server deafen remain voice state flags in a peer-to-peer mesh, and each peer applies them to the media it sends and plays. A peer closes its peer connection to a connection whose voice state leaves the channel.
|
||||
|
||||
### <span id="voice-signal"></span>VOICE_SIGNAL
|
||||
|
||||
Another connection of a [peer-to-peer](#peer-to-peer-voice) voice channel or call sent this session a signalling message with the [Voice Signal](/gateway/commands/#voice-signal) command. Delivered to the session that owns the target connection alone.
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| guild_id?<sup>1</sup> | snowflake | The guild the channel belongs to |
|
||||
| channel_id | snowflake | The voice channel or call channel |
|
||||
| from | string | The `connection_id` of the sending voice state |
|
||||
| user_id | snowflake | The user who owns the sending connection |
|
||||
| data | object | The signalling message exactly as the sender supplied it |
|
||||
|
||||
<sup>1</sup> Present for a guild voice channel and absent for a call
|
||||
|
||||
```json
|
||||
{
|
||||
"guild_id": "1189375284394692608",
|
||||
"channel_id": "1189375284394692610",
|
||||
"from": "9a41d07c5be2f318",
|
||||
"user_id": "1189375284394692700",
|
||||
"data": {
|
||||
"type": "answer",
|
||||
"sdp": "v=0..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Fluxer authorises every signal against the current voice states, so `from` always names a connection with `p2p: true` in `channel_id`. A client discards a signal whose `from` it no longer has in its roster.
|
||||
|
||||
### <span id="entrance-sound-play"></span>ENTRANCE_SOUND_PLAY
|
||||
|
||||
A participant asked for their entrance sound to play in a voice channel they are already connected to.
|
||||
|
||||
@@ -8,7 +8,7 @@ Every main Gateway connection has limits on what it sends, how long its session
|
||||
|
||||
## Transport and encoding
|
||||
|
||||
[Framing](/gateway/overview/#framing) owns the protocol version, the payload bound, and the compression contract. One inbound WebSocket message is limited to 4,096 bytes on the wire and to a further 4,096 bytes after decompression, and either bound closes with `4002` and reason `Payload too large`.
|
||||
[Framing](/gateway/overview/#framing) owns the protocol version, the payload bound, and the compression contract. One inbound WebSocket message is limited to 16,384 bytes on the wire and to a further 16,384 bytes after decompression, and either bound closes with `4002` and reason `Payload too large`.
|
||||
|
||||
A compressed message that decompresses past 10 MiB closes with `4002` and reason `Decompression failed`. That message never closes with `Payload too large`.
|
||||
|
||||
@@ -50,6 +50,8 @@ Presence Update accepts five commands per WebSocket in a rolling 20-second windo
|
||||
|
||||
Voice State Update processes the first two commands per session in a rolling one-second window immediately. Later updates enter a per-session queue that holds at most 64 commands and drains one command every 500 ms. A newer update replaces an older queued update for the same `guild_id` and `connection_id` pair, and a full queue discards its oldest entry before accepting the new one.
|
||||
|
||||
Voice Signal accepts 240 commands per WebSocket in a rolling 10-second window, which leaves room for a burst of trickle ICE candidates. A further signal is discarded without closing the connection. Every signal, discarded or not, also counts toward the three payload budgets above.
|
||||
|
||||
Request Guild Members has three command-specific budgets. Each account may send 12 requests in a rolling 10-second window, and each guild accepts 40 requests in a rolling 10-second window across all accounts. A request over either of these budgets is discarded without a result and without a close. A bot requesting a complete member list is limited to one accepted request per guild every 30 seconds, and a request inside that window produces [Rate Limited](/gateway/events/#rate-limited). The budget is keyed by the account and the guild together, so reconnecting does not reset it.
|
||||
|
||||
## Bounded commands
|
||||
@@ -94,8 +96,10 @@ Identify accepts at most 256 `ignored_events` entries. A longer array closes wit
|
||||
|
||||
## Voice admission
|
||||
|
||||
Voice admission follows the enclosing guild, DM, group DM, channel, and permission rules. A refusal produces no event and closes nothing.
|
||||
Voice admission follows the enclosing guild, DM, group DM, channel, and permission rules. A refusal closes nothing. It produces a [Gateway Error](/gateway/opcodes-and-close-codes/#gateway-error) for `VOICE_CHANNEL_FULL`, `VOICE_P2P_CONSENT_REQUIRED`, and `VOICE_P2P_UNAVAILABLE`, and no payload for every other code.
|
||||
|
||||
A guild voice channel admits at most `user_limit` users, where `0` means unlimited. A channel in which any participant has a camera enabled also admits at most 25 users in total, whatever its `user_limit`. A channel that already holds 25 users with cameras enabled refuses a further camera with `VOICE_CAMERA_USER_LIMIT`. [Capacity](/voice/#capacity) states these bounds in full.
|
||||
|
||||
A peer-to-peer mesh holds at most 4 participants, counted as connections in a guild voice channel and as users in a call. A join past that bound is refused with `VOICE_CHANNEL_FULL`, as [Peer-to-peer voice](/gateway/events/#peer-to-peer-voice) describes.
|
||||
|
||||
One user holds at most `voice_connection_limit` simultaneous voice connections in one guild voice channel. The field is part of the [channel object](/http-api/channels/#channel-object), defaults to 5, and is accepted from 1 through 100. Pending connections that have not yet expired count against it.
|
||||
@@ -22,7 +22,8 @@ An opcode is the number that names a [Gateway payload](/gateway/overview/#gatewa
|
||||
| 9 | Invalid Session<sup>1</sup> | Server to client |
|
||||
| 10 | Hello | Server to client |
|
||||
| 11 | Heartbeat ACK | Server to client |
|
||||
| 12 | Gateway Error | Reserved |
|
||||
| 12 | Gateway Error | Server to client |
|
||||
| 13 | Voice Signal | Client to server |
|
||||
| 14 | Lazy Request | Client to server |
|
||||
| 15 | Request Guild Counts | Client to server |
|
||||
| 16 | Request Channel Member Counts | Client to server |
|
||||
@@ -46,7 +47,8 @@ An opcode is the number that names a [Gateway payload](/gateway/overview/#gatewa
|
||||
| 9 | Literal `false` | The session named by Resume could not be reached, the supplied `seq` is below the replay floor, or an established session ended<sup>6</sup> |
|
||||
| 10 | Object with `heartbeat_interval` | Sent as the WebSocket is accepted, before any client payload is read |
|
||||
| 11 | No `d` field | Acknowledges an accepted client heartbeat |
|
||||
| 12 | Not implemented | Never sent. Errors reach the client as a Dispatch or a close frame |
|
||||
| 12 | [Gateway Error object](#gateway-error) | Fluxer reports a refused voice join that the client is expected to act on |
|
||||
| 13 | [Voice Signal object](/gateway/commands/#voice-signal) | The client sends peer-to-peer signalling to one other voice connection |
|
||||
| 14 | [Lazy Request object](/gateway/commands/#lazy-request) | The client replaces bounded guild subscriptions |
|
||||
| 15 | [Request Guild Counts object](/gateway/commands/#request-guild-counts) | The client requests current guild count records |
|
||||
| 16 | [Request Channel Member Counts object](/gateway/commands/#request-channel-member-counts) | The client requests channel count records |
|
||||
@@ -56,7 +58,7 @@ An opcode is the number that names a [Gateway payload](/gateway/overview/#gatewa
|
||||
|
||||
<sup>2</sup> Answer a server heartbeat request immediately, in addition to the regular schedule
|
||||
|
||||
<sup>3</sup> A client that sends Opcode 5 or 12 gets the same close as a client that sent an undefined opcode
|
||||
<sup>3</sup> A client that sends Opcode 5 or 12 gets the same close as a client that sent an undefined opcode. Fluxer never sends Opcode 5
|
||||
|
||||
<sup>4</sup> Resume is accepted whether or not a session is already attached to the connection
|
||||
|
||||
@@ -64,7 +66,7 @@ An opcode is the number that names a [Gateway payload](/gateway/overview/#gatewa
|
||||
|
||||
<sup>6</sup> After the frame, a socket whose session ended is unauthenticated. After a failed Resume, a socket that already held a session still holds it
|
||||
|
||||
The registry is complete. Opcode 13, the values 17 through 27, and every value above 28 are undefined.
|
||||
The registry is complete. The values 17 through 27 and every value above 28 are undefined.
|
||||
|
||||
Fluxer resolves an inbound payload in this order.
|
||||
|
||||
@@ -72,7 +74,7 @@ Fluxer resolves an inbound payload in this order.
|
||||
2. Heartbeat and Resume are handled whether or not a session is attached.
|
||||
3. Identify closes with `4005` when a session is already attached.
|
||||
4. Every remaining opcode closes with `4003` while no session is attached.
|
||||
5. With a session attached, Presence Update, Voice State Update, Request Guild Members, Lazy Request, Request Guild Counts, Request Channel Member Counts, and Request Forum Unreads are handled. Every other opcode, including a server opcode and an undefined value, closes with `4001`.
|
||||
5. With a session attached, Presence Update, Voice State Update, Voice Signal, Request Guild Members, Lazy Request, Request Guild Counts, Request Channel Member Counts, and Request Forum Unreads are handled. Every other opcode, including a server opcode and an undefined value, closes with `4001`.
|
||||
|
||||
Request Forum Unreads from a user session that did not set [`CHANNEL_THREADS`](/gateway/threads/#session-flag) also closes with `4001`.
|
||||
|
||||
@@ -80,6 +82,33 @@ Request Forum Unreads from a user session that did not set [`CHANNEL_THREADS`](/
|
||||
A client SHOULD log an unknown opcode and ignore the frame, and MUST NOT close or reconnect solely because the server used an opcode newer than this registry.
|
||||
:::
|
||||
|
||||
## Gateway Error
|
||||
|
||||
Opcode `12` reports a refused [Voice State Update](/gateway/commands/#voice-state-update) command to the session that sent it. The payload has no `s` and no `t`, is not replayed by [Resume](/gateway/commands/#resume), and closes nothing.
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| code | string | The refusal code |
|
||||
| message | string | A description for diagnosis, which a client does not show or parse |
|
||||
|
||||
```json
|
||||
{
|
||||
"op": 12,
|
||||
"d": {
|
||||
"code": "VOICE_P2P_CONSENT_REQUIRED",
|
||||
"message": "This call is peer-to-peer and requires agreement to join"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Fluxer sends three codes.
|
||||
|
||||
- `VOICE_P2P_CONSENT_REQUIRED` means the channel or call is peer-to-peer and the join did not send `p2p: true`. Ask the user, then send the join again with `p2p: true`.
|
||||
- `VOICE_P2P_UNAVAILABLE` means Fluxer declined peer-to-peer for a join into a peer-to-peer channel or call. Abort the join and tell the user.
|
||||
- `VOICE_CHANNEL_FULL` means the channel is at its `user_limit`, or the peer-to-peer mesh is at its participant cap. Abort the join and tell the user.
|
||||
|
||||
Every other voice refusal sends no payload. [Peer-to-peer voice](/gateway/events/#peer-to-peer-voice) states when each of the first two codes is produced.
|
||||
|
||||
## Close codes
|
||||
|
||||
| Code | Name | Meaning |
|
||||
@@ -185,7 +214,7 @@ The Gateway sends an exact reason string with every application close.
|
||||
Reason strings are stable wire values. A client branches on the code and MAY record the reason for diagnosis.
|
||||
|
||||
:::note[Some refusals send nothing]
|
||||
A held or discarded Identify, an over-budget Presence Update, an over-budget [Request Forum Unreads](/gateway/threads/#request-forum-unreads), a dropped bounded request, and a rejected voice state update produce no close and no reason. [Client commands](/gateway/commands/) states which command behaves that way.
|
||||
A held or discarded Identify, an over-budget Presence Update, an over-budget [Request Forum Unreads](/gateway/threads/#request-forum-unreads), a dropped bounded request, an unroutable or over-budget Voice Signal, and a rejected voice state update produce no close and no reason. A rejected voice state update sends a [Gateway Error](#gateway-error) for three codes and nothing for the rest. [Client commands](/gateway/commands/) states which command behaves that way.
|
||||
:::
|
||||
|
||||
## Ordinary WebSocket closes
|
||||
|
||||
@@ -90,7 +90,7 @@ A replayed Dispatch keeps its original sequence, and a replayed run can have gap
|
||||
|
||||
## Framing
|
||||
|
||||
One inbound WebSocket message is limited to 4,096 bytes on the wire, and a compressed inbound message is limited to a further 4,096 bytes after decompression. A message past either bound closes with `4002` and reason `Payload too large`. An inbound message that cannot be decompressed closes with `4002` and reason `Decompression failed`.
|
||||
One inbound WebSocket message is limited to 16,384 bytes on the wire, and a compressed inbound message is limited to a further 16,384 bytes after decompression. A message past either bound closes with `4002` and reason `Payload too large`. An inbound message that cannot be decompressed closes with `4002` and reason `Decompression failed`.
|
||||
|
||||
JSON payloads are UTF-8 objects. An uncompressed client payload is sent in a text frame, and a payload the client compressed with the negotiated zstd stream is sent in a binary frame.
|
||||
|
||||
|
||||
@@ -100,6 +100,8 @@ Changes the RTC region of an active direct message or group direct message call.
|
||||
|
||||
The caller does not have to be connected to the call.
|
||||
|
||||
Supplying `region` on a [peer-to-peer](/gateway/events/#peer-to-peer-voice) call moves the call to a voice server, even when the value names the region the call already has. The move is one way for the life of that call.
|
||||
|
||||
### Path parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
|
||||
@@ -32,6 +32,7 @@ A channel object always has its identity and type. Every other field is present
|
||||
| user_limit?<sup>4</sup> | ?integer | The configured member occupancy limit, present only for a guild voice channel |
|
||||
| voice_connection_limit?<sup>5</sup> | ?integer | The number of simultaneous voice connections one user may hold, present only for a guild voice channel |
|
||||
| rtc_region?<sup>6</sup> | ?string | The ID of the selected RTC region, present only for a guild voice channel |
|
||||
| rtc_p2p? | boolean | Whether the channel is pinned to [peer-to-peer voice](/gateway/events/#peer-to-peer-voice), present only for a guild voice channel |
|
||||
| last_message_id? | ?snowflake | The ID of the most recent message, null when the channel has none |
|
||||
| last_pin_timestamp? | ?ISO8601 timestamp | The time a message was most recently pinned, or null when nothing has ever been pinned |
|
||||
| permission_overwrites?<sup>7</sup> | array[[permission overwrite](#permission-overwrite-object) object] | The overwrites applied to this channel, present only for a guild channel other than a thread |
|
||||
@@ -464,6 +465,7 @@ Every field is optional, and an omitted field preserves its current value. Every
|
||||
| content_warning_text?<sup>10</sup> | ?string | The content warning text (max 200 characters) |
|
||||
| rate_limit_per_user?<sup>11</sup> | ?integer | The slowmode interval in seconds (0-21,600) |
|
||||
| rtc_region?<sup>12</sup> | ?string | The ID of the RTC region (1-64 characters) for a guild voice channel, where null selects automatic routing |
|
||||
| rtc_p2p? | boolean | Whether to pin a guild voice channel to [peer-to-peer voice](/gateway/events/#peer-to-peer-voice). Ignored on every other channel type |
|
||||
|
||||
<sup>1</sup> An explicit null preserves the current name. In a guild without flexible channel names, Fluxer lowercases a guild text, announcement, forum, or media channel name, replaces its whitespace with hyphens, and removes disallowed punctuation
|
||||
|
||||
@@ -572,7 +574,7 @@ A group marked for mature content accepts media the scanner flags as sensitive,
|
||||
|
||||
A guild channel change emits [Channel Update](/gateway/events/#channel-update) to every session subscribed to its guild. For a forum or media channel the event reaches only a session with [thread access](/gateway/threads/#session-flag). Replacing a category's permission overwrites also replaces them on each child whose overwrites still exactly match the category's previous values, with one Dispatch per changed child. A child whose overwrites had differed is left unchanged.
|
||||
|
||||
Changing `rate_limit_per_user` clears the remaining slowmode delay of every user in the channel. Each user's next message is then checked against the new interval. Changing `rtc_region` on a guild voice channel moves its active voice connections to the selected region.
|
||||
Changing `rate_limit_per_user` clears the remaining slowmode delay of every user in the channel. Each user's next message is then checked against the new interval. Changing `rtc_region` on a guild voice channel moves its active voice connections to the selected region. A [peer-to-peer](/gateway/events/#peer-to-peer-voice) call in the channel moves to a voice server with that change, unless the channel stores `rtc_p2p` true. Changing `rtc_p2p` leaves a running call as it is and applies to the next call that starts after the channel empties. Changing it to true fails with the field code `GUILD_FEATURE_NOT_TOGGLEABLE` unless the caller's [peer-to-peer voice assignment](/http-api/experiments/#peer-to-peer-voice-assignment-object) is enabled.
|
||||
|
||||
Converting an announcement channel into a text channel deletes every [channel follower webhook](/http-api/webhooks/#webhook-types) that follows it. The deletion runs in the background after the response, emits [Webhooks Update](/gateway/events/#webhooks-update) for each target channel, and writes no audit log entry. Copies already delivered stay in the target channels, and later edits and deletions of the published messages still reach them.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: The experiment assignments envelope, the revalidation and polling c
|
||||
|
||||
import RouteHeader from '@/components/RouteHeader.astro';
|
||||
|
||||
An experiment is an instance-wide rollout. For each account, Fluxer works out whether the account is in the rollout and which settings the account receives. The single route on this page resolves every experiment the server defines and returns them in one envelope, together with the polling cadence they share. This server defines `domain_migration`, `plutonium_page`, and `channel_threads`. Only `domain_migration` is configured by an operator. `plutonium_page` and `channel_threads` are on for every account.
|
||||
An experiment is an instance-wide rollout. For each account, Fluxer works out whether the account is in the rollout and which settings the account receives. The single route on this page resolves every experiment the server defines and returns them in one envelope, together with the polling cadence they share. This server defines `domain_migration`, `plutonium_page`, `channel_threads`, and `voice_p2p`. Only `domain_migration` and `voice_p2p` are configured by an operator. `plutonium_page` and `channel_threads` are on for every account.
|
||||
|
||||
Every assignment is advice. A client that ignores one behaves as it does with the rollout off, and no route and no Gateway event reports what a client actually ran.
|
||||
|
||||
@@ -35,6 +35,7 @@ One entry per experiment. The envelope reports this object even when it is empty
|
||||
| domain_migration? | [domain migration assignment](#domain-migration-assignment-object) object | The caller's web domain migration assignment |
|
||||
| plutonium_page? | [Plutonium page assignment](#plutonium-page-assignment-object) object | The caller's Plutonium page assignment |
|
||||
| channel_threads? | [channel threads assignment](#channel-threads-assignment-object) object | The caller's channel threads assignment |
|
||||
| voice_p2p? | [peer-to-peer voice assignment](#peer-to-peer-voice-assignment-object) object | The caller's peer-to-peer voice assignment |
|
||||
|
||||
Ignore unknown experiments and treat a missing experiment as off.
|
||||
|
||||
@@ -48,7 +49,7 @@ One resolution of the instance web domain migration rollout against one account.
|
||||
| --- | --- | --- |
|
||||
| enabled | boolean | Whether the caller's web client moves to the new web origin |
|
||||
|
||||
A caller is drawn by the operator's account or guild allowlist, by having premium when the operator includes premium users, or by the sampled share of the account population. `enabled` is false in every other case, the rollout being off included.
|
||||
A caller is drawn by the operator's account or guild allowlist, by having premium when the operator includes premium users, or by the sampled share of the account population. When the operator lists rollout countries, the sampled share only draws a caller whose request comes from a listed country. `enabled` is false in every other case, the rollout being off included.
|
||||
|
||||
Only the official web client acts on this assignment, and only on its legacy origins. Every other client ignores it. The logged-out share and the instance-wide switch are published in the [instance discovery document](/http-api/instance/#domain-migration-object) instead, because a client that holds no credential cannot read this route.
|
||||
|
||||
@@ -79,6 +80,25 @@ The channel threads state for one account. This server version writes the key on
|
||||
|
||||
The key stays for clients that still read it. Threads, forum channels, and media channels need the [`channel_threads` client capability](/http-api/threads/#client-capability).
|
||||
|
||||
## Peer-to-peer voice assignment object
|
||||
|
||||
One resolution of the instance peer-to-peer voice rollout against one account. This server version writes the key on every response.
|
||||
|
||||
### Structure
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| enabled | boolean | Whether the caller may start a peer-to-peer voice call |
|
||||
| max_participants | integer | Most participants a peer-to-peer call holds (2-4) |
|
||||
|
||||
A caller is drawn by the operator's account or guild allowlist, by having premium when the operator includes premium users, or by the sampled share of the account population. When the operator lists rollout countries, the sampled share only draws a caller whose request comes from a listed country. `enabled` is false in every other case, the rollout being off included.
|
||||
|
||||
A selected client may ask for a peer-to-peer connection when it starts a voice call, and shows the peer-to-peer setting of a guild voice channel. It offers a peer-to-peer call in a direct message or group direct message only when the channel has at most `max_participants` members, the caller included. `max_participants` is the same for every account, selected or not, and a join past it is [refused as full](/gateway/events/#peer-to-peer-voice). The assignment does not decide who joins. While the rollout is on, any account that agrees to a direct connection may join a peer-to-peer call that is already running. The server checks the rollout again when a call starts, so a client that ignores this assignment gains nothing.
|
||||
|
||||
## Request country
|
||||
|
||||
Fluxer resolves the country of each request to this route from the client address. An account can receive a different assignment from another network when an operator lists rollout countries. A request whose country is unknown matches no listed country.
|
||||
|
||||
## Get experiment assignments
|
||||
|
||||
<RouteHeader method="GET" path="/v1/experiments" bot />
|
||||
@@ -112,7 +132,7 @@ A client sends the `ETag` it last received. Fluxer compares it against the tag o
|
||||
| Cache-Control | string | The literal value `private, no-cache` |
|
||||
| Vary | string | The literal value `Authorization`, replaced by `Origin` where the [cross-origin policy](/http-api/#cross-origin-requests) echoed an allowed origin |
|
||||
|
||||
The `ETag` changes when the response changes. Treat it as an opaque value.
|
||||
The `ETag` changes when the response changes, which includes a change of [request country](#request-country). Treat it as an opaque value.
|
||||
|
||||
`ETag` is listed in `Access-Control-Expose-Headers` and `If-None-Match` in `Access-Control-Allow-Headers`, so a cross-origin client reads the tag and revalidates with it.
|
||||
|
||||
|
||||
@@ -311,6 +311,7 @@ Recorded by [CHANNEL_CREATE](#audit-actions), [CHANNEL_UPDATE](#audit-actions),
|
||||
| voice_connection_limit | ?number | The configured per-user voice connection limit |
|
||||
| bitrate | ?number | The voice bitrate in bits per second |
|
||||
| rtc_region | ?string | The pinned voice region, or null when the channel is routed automatically |
|
||||
| rtc_p2p | boolean | Whether the channel is pinned to peer-to-peer voice |
|
||||
| permission_overwrite_count<sup>2</sup> | number | The number of permission overwrites the channel has |
|
||||
| flags<sup>3</sup> | number | The [channel flags](/http-api/forums/#channel-flags) of the channel |
|
||||
| default_auto_archive_duration<sup>3</sup> | number | The stored default [auto archive duration](/http-api/threads/#auto-archive-durations) |
|
||||
|
||||
@@ -308,7 +308,7 @@ Hierarchy authority over the target is not checked for `roles`, so a caller who
|
||||
|
||||
Fluxer never checks the target's own permissions in the destination, so a member can be moved into a channel they could not join themselves. Disconnecting sets `channel_id` to null and requires the same permission.
|
||||
|
||||
Supplying `channel_id` for a target with no live voice connection, or a `connection_id` that is not one of the target's own, returns 400 `USER_NOT_IN_VOICE`. Supplying `mute` or `deaf` for such a target succeeds and stores the flags. A destination that does not exist returns the field code `CHANNEL_DOES_NOT_EXIST`, and one that is not a voice channel `CHANNEL_MUST_BE_VOICE`.
|
||||
Supplying `channel_id` for a target with no live voice connection, or a `connection_id` that is not one of the target's own, returns 400 `USER_NOT_IN_VOICE`. Supplying `mute` or `deaf` for such a target succeeds and stores the flags. A destination that does not exist returns the field code `CHANNEL_DOES_NOT_EXIST`, and one that is not a voice channel `CHANNEL_MUST_BE_VOICE`. A destination that is [peer-to-peer](/gateway/events/#peer-to-peer-voice) also returns 400 `USER_NOT_IN_VOICE`.
|
||||
|
||||
`MANAGE_ROLES` and `MODERATE_MEMBERS` are [elevated permissions](/http-api/permissions/#elevated-permissions). When the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated, a caller who is not the guild owner also needs an enrolled multi-factor authenticator to replace `roles` or apply `communication_disabled_until`.
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
title: Peer-to-peer voice
|
||||
description: Connection outcome reports for the peer connections of a peer-to-peer voice call.
|
||||
---
|
||||
|
||||
import RouteHeader from '@/components/RouteHeader.astro';
|
||||
|
||||
A peer-to-peer voice call connects its participants directly, as [Peer-to-peer voice](/gateway/events/#peer-to-peer-voice) describes. Fluxer hands out STUN servers only and never relays media, so whether two participants connect depends on their networks. Clients report how each peer connection settled, so an operator can see which networks connect directly.
|
||||
|
||||
The route on this page is user-only. Fluxer rejects a bot or OAuth2 credential with 403 `ACCESS_DENIED`.
|
||||
|
||||
## Connection report object
|
||||
|
||||
The outcome of one peer connection, as seen from the reporting client. A client sends one report for each remote peer of a call, once the connection to that peer reaches connected or fails.
|
||||
|
||||
### Structure
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| channel_id<sup>1</sup> | snowflake | The ID of the guild voice channel or private channel of the call |
|
||||
| guild_id | ?snowflake | The ID of the guild of the channel, null for a call |
|
||||
| participant_count | integer | Participants in the call when the connection settled (2-4) |
|
||||
| outcome | string | `connected` or `failed` |
|
||||
| local_candidate_type<sup>2</sup> | ?string | Candidate type of the local end of the selected pair |
|
||||
| remote_candidate_type<sup>2</sup> | ?string | Candidate type of the remote end of the selected pair |
|
||||
| ip_family<sup>3</sup> | ?string | `ipv4` or `ipv6`, the address family of the selected pair |
|
||||
| protocol<sup>3</sup> | ?string | `udp` or `tcp`, the transport of the selected pair |
|
||||
| setup_ms<sup>4</sup> | ?integer | Milliseconds from creating the connection to its first connected state (0-2,147,483,647) |
|
||||
| ice_restarted | boolean | Whether the connection ran an ICE restart |
|
||||
|
||||
<sup>1</sup> Fluxer stores the value with no check that the channel exists or that the caller was in its call
|
||||
|
||||
<sup>2</sup> One of `host`, `srflx`, `prflx`, or `relay`, and null when the connection has no selected candidate pair
|
||||
|
||||
<sup>3</sup> Null when the connection has no selected candidate pair
|
||||
|
||||
<sup>4</sup> Null for a connection that never reached connected
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_id": "1189375284394692610",
|
||||
"guild_id": null,
|
||||
"participant_count": 2,
|
||||
"outcome": "connected",
|
||||
"local_candidate_type": "srflx",
|
||||
"remote_candidate_type": "host",
|
||||
"ip_family": "ipv6",
|
||||
"protocol": "udp",
|
||||
"setup_ms": 840,
|
||||
"ice_restarted": false
|
||||
}
|
||||
```
|
||||
|
||||
## Report connection outcomes
|
||||
|
||||
<RouteHeader method="POST" path="/v1/voice/p2p/connection-reports" />
|
||||
|
||||
Records 1 through 8 [connection report objects](#connection-report-object) and returns 204 with an empty body. User-only. Emits no Gateway event.
|
||||
|
||||
A client batches its reports and sends them best effort. A failed or rate-limited request changes nothing about the call, and a client drops the batch rather than retrying more than once.
|
||||
|
||||
### JSON body
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| reports<sup>1</sup> | array[[connection report](#connection-report-object) object] | The reports to record (1-8) |
|
||||
|
||||
<sup>1</sup> A list of fewer than 1 or more than 8 entries, or any invalid entry, fails with 400 `INVALID_FORM_BODY` and records nothing
|
||||
|
||||
### Response
|
||||
|
||||
| Status | Body | Condition |
|
||||
| --- | --- | --- |
|
||||
| 204 | empty | Every report was recorded, or peer-to-peer voice is disabled on the instance and nothing was recorded |
|
||||
| 400 | [error response](/http-api/#error-response) | The body is invalid and the request returns `INVALID_FORM_BODY` |
|
||||
| 403 | [error response](/http-api/#error-response) | Caller is a bot or presents a bearer credential and the request returns `ACCESS_DENIED` |
|
||||
|
||||
### Side effects
|
||||
|
||||
Fluxer stores each report with the caller's user ID, the time of the request, the request address and its country, and the client platform and operating system of the caller's session. The country is null when the address resolves to none. Reports expire after 180 days.
|
||||
|
||||
Fluxer keeps each report for 180 days. No route reads reports back.
|
||||
|
||||
### Rate limit
|
||||
|
||||
10 requests per minute for each authenticated user, on the `voice:p2p:connection_reports` bucket.
|
||||
@@ -54,7 +54,7 @@ A thread is a [channel object](/http-api/channels/#channel-object) with the thre
|
||||
|
||||
<sup>6</sup> Present only when the caller is a member, on [Get channel](/http-api/channels/#get-channel), on [Modify channel](/http-api/channels/#modify-channel), on `thread` of a message, on [Thread Create](/gateway/threads/#thread-create), and on the `threads` list of a guild in [Ready](/gateway/events/#ready), [Guild Create](/gateway/events/#guild-create), and [Guild Sync](/gateway/events/#guild-sync)
|
||||
|
||||
The schema also declares `topic`, `url`, `icon`, `position`, `bitrate`, `user_limit`, `voice_connection_limit`, `rtc_region`, `permission_overwrites`, `recipients`, `nsfw`, `nsfw_override`, `content_warning_level`, `content_warning_text`, `nicks`, `default_auto_archive_duration`, `default_thread_rate_limit_per_user`, `available_tags`, `default_reaction_emoji`, `default_sort_order`, `default_forum_layout`, and `default_tag_setting`. Fluxer never sends them on a thread. A thread uses the permissions of its parent as [Thread permissions](#thread-permissions) describes. An age restriction and a content warning resolve through the parent, then the parent category, and finally the guild.
|
||||
The schema also declares `topic`, `url`, `icon`, `position`, `bitrate`, `user_limit`, `voice_connection_limit`, `rtc_region`, `rtc_p2p`, `permission_overwrites`, `recipients`, `nsfw`, `nsfw_override`, `content_warning_level`, `content_warning_text`, `nicks`, `default_auto_archive_duration`, `default_thread_rate_limit_per_user`, `available_tags`, `default_reaction_emoji`, `default_sort_order`, `default_forum_layout`, and `default_tag_setting`. Fluxer never sends them on a thread. A thread uses the permissions of its parent as [Thread permissions](#thread-permissions) describes. An age restriction and a content warning resolve through the parent, then the parent category, and finally the guild.
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
@@ -744,6 +744,16 @@ Moving a media port is one line in `.env` followed by `docker compose up -d live
|
||||
|
||||
To rotate the key pair, set both names in `.env`, then run `docker compose up -d livekit api worker` to apply the change everywhere.
|
||||
|
||||
### Peer-to-peer calls
|
||||
|
||||
A peer-to-peer call sends media directly between its participants and not through LiveKit or any relay. It runs only while the `voice_p2p` experiment is enabled in the [instance configuration](/admin-api/instance/). The value below names the STUN servers Fluxer hands to each participant. It is optional and Compose forwards it from `.env`. The stack ships no STUN server.
|
||||
|
||||
#### `FLUXER_VOICE_P2P_STUN_URLS`
|
||||
|
||||
Default empty. A comma-separated list of `stun:` URLs. Participants use them to discover their public addresses.
|
||||
|
||||
Unset, participants receive no ICE servers and connect only when they can already reach each other, as on one local network. Two participants that cannot reach each other directly fail to connect, and either one can switch the call to LiveKit. Participants in a peer-to-peer call can learn each other's IP addresses.
|
||||
|
||||
The `Caddyfile` is a bind mount, so the edge reads the copy that sits on disk beside `docker-compose.yml`. Editing it takes `docker compose restart edge`, because `docker compose up -d` leaves a container alone when only a mounted file changed. An upgrade does that restart itself, which [What the script does](/operator/upgrading/#what-the-script-does) covers. A change to any LiveKit value in `.env` needs `docker compose up -d`, because `restart` reuses the existing container with its old environment.
|
||||
|
||||
## Email
|
||||
|
||||
@@ -28,6 +28,8 @@ Media never crosses the HTTP API. No route returns an audio or video track, the
|
||||
|
||||
Fluxer runs media over LiveKit and publishes no signalling protocol of its own. There is no voice websocket, no voice opcode set, no UDP discovery step, and no separate encryption handshake. A client connects to the `endpoint` the [Voice Server Update](/gateway/events/#voice-server-update) grant names, presents the grant `token`, and speaks the LiveKit protocol from there.
|
||||
|
||||
A peer-to-peer grant is the one exception. It has ICE servers and no `endpoint` or `token`, and the peers signal through the Gateway as [Peer-to-peer voice](/gateway/events/#peer-to-peer-voice) describes.
|
||||
|
||||
| Concept | Value |
|
||||
| --- | --- |
|
||||
| Room name, guild voice channel | `guild_{guild_id}_channel_{channel_id}` |
|
||||
@@ -67,11 +69,11 @@ A grant is issued when a connection opens, when it moves to another channel, and
|
||||
|
||||
The grant `token` is issued for the media server and consumed by the media connection alone. No route on this API accepts it.
|
||||
|
||||
Fluxer reports a refusal by sending no Dispatch. A client observes a refused placement only as the absence of a grant.
|
||||
Fluxer reports a refusal by sending no Dispatch. A client observes most refused placements only as the absence of a grant. A refusal with `VOICE_CHANNEL_FULL`, `VOICE_P2P_CONSENT_REQUIRED`, or `VOICE_P2P_UNAVAILABLE` also sends the requesting session a [Gateway Error](/gateway/opcodes-and-close-codes/#gateway-error) payload.
|
||||
|
||||
## Guild voice channels
|
||||
|
||||
A guild voice channel stores its `bitrate`, `user_limit`, `voice_connection_limit`, and `rtc_region` on the [channel object](/http-api/channels/#channel-object). It also has ordinary messages, pins, and slowmode, so its text history is read and written through the [Messages resource](/http-api/messages/).
|
||||
A guild voice channel stores its `bitrate`, `user_limit`, `voice_connection_limit`, `rtc_region`, and `rtc_p2p` on the [channel object](/http-api/channels/#channel-object). It also has ordinary messages, pins, and slowmode, so its text history is read and written through the [Messages resource](/http-api/messages/).
|
||||
|
||||
A new voice channel stores a `bitrate` of 64000. The ceiling is 96000, and the `AUDIO_BITRATE_128_KBPS`, `AUDIO_BITRATE_256_KBPS`, and `AUDIO_BITRATE_384_KBPS` [guild features](/http-api/guilds/#guild-features) raise it to 128000, 256000, and 384000. A direct message and a group direct message call have no `bitrate` and always run at 64000.
|
||||
|
||||
|
||||
Reference in new issue
Block a user