This specification defines the private voice chat protocol, designed to provide voice communication between two users in the Explorer, simulating a two person call.
After having had the voice chat for nearby users in previous versions of Decentraland, the need of having private voice conversations between users was identified as an important feature.
In this new feature, a user is able to ring another user (from now on the caller and callee, respectively), initiating the call flow. The callee has the capability of rejecting this call or accepting it and the caller can cancel the process at any time. If the callee rejects the call or the caller cancels it, the ringing process ends, notifying the user of the action taken by the other party. Case contrary, by accepting the call, the callee and the caller will both receive tokens to connect to the real time service that will facilitate the call (LiveKit at the time of this ADR). Either user can end the call at any time, disconnecting the other and finalizing the call.
The feature includes certain restrictions that must be enforced to improve the user experience:
Following the user restrictions mentioned above, there are some restrictions for the voice chat that are required to be enforced as well:
These restrictions are enforced by either the Social Services (via private voice chat updates) or the Comms Gatekeeper (via LiveKit actions).
This section specifies the interaction between the Explorer and the Social Service for the purposes of ringing someone, receiving voice chat updates, and using LiveKit for real-time voice interactions.
Starting a private voice chat is done by performing the StartPrivateVoiceChat
RPC
call with the callee address to the Social Service. If the process complies with the
requirements mentioned in the (Context)[#Context] section, the call process will
initiate successfully.
It must be taken into consideration that this is an asynchronous flow, meaning that if the start process is successful, it will return immediately with a call ID. This call ID can be used to identify the call and end it. The callee will be notified through the updates subscription mechanism mentioned in the (Subscribing private voice chats updates)[#Subscribing-private-voice-chats-updates] section.
Here's the interaction flow of the Explorer and the Social Service:
sequenceDiagram
participant U1 as Caller
participant SS as Social Service
U1->>SS: Start private voice chat with callee
alt Caller or callee allow only calls from friends and they're not friends
SS-->>U1: Forbidden error
else Caller or callee are ringing another user
SS-->>U1: Conflicting error
else Caller or callee are in another private voice chat
SS-->>U1: Conflicting error
else Callee and caller can start a conversation
SS-->>U1: Call id
end
The RPC call is specified in the protocol as:
message StartPrivateVoiceChatPayload {
User callee = 1;
}
message StartPrivateVoiceChatResponse {
message Ok {
string call_id = 1;
}
oneof response {
Ok ok = 1;
InternalServerError internal_server_error = 2;
ConflictingError conflicting_error = 4;
ForbiddenError forbidden_error = 5;
}
}
rpc StartPrivateVoiceChat(StartPrivateVoiceChatPayload) returns (StartPrivateVoiceChatResponse) {}
Rejecting a private voice chat is done by performing the
RejectPrivateVoiceChat
RPC call to the Social Service with the call id of the
ringing voice chat. If successful, it will end the ringing process without returning any
errors. The caller will be notified through the updates subscription mechanism mentioned in
the (Subscribing private voice chats updates)[#Subscribing-private-voice-chats-updates] section.
Here's the interaction flow of the Explorer and the Social Service:
sequenceDiagram
actor U2 as Callee
participant SS as Social Server
U2->>SS: Reject call
alt The call doesn't exist
SS-->>U2: Not found error
else The call exists
SS-->>U2: Ok
end
The RPC call is specified in the protocol as:
message RejectPrivateVoiceChatPayload {
string call_id = 1;
}
message RejectPrivateVoiceChatResponse {
message Ok {
string call_id = 1;
}
oneof response {
Ok ok = 1;
InternalServerError internal_server_error = 2;
NotFoundError not_found = 4;
}
}
rpc RejectPrivateVoiceChat(RejectPrivateVoiceChatPayload) returns (RejectPrivateVoiceChatResponse) {}
Accepting a private voice chat is done by performing the
AcceptPrivateVoiceChat
RPC call to the Social Service with the call id of the
ringing voice chat. If successful, it will return the connection URL, which is composed of
LiveKit's connection URL and the token to connect to the LiveKit room. Case contrary, the
voice chat must be finished in the client, as this means that the voice chat had either
expired or been cancelled.
This process will result in the caller being notified through the subscription's updates mechanism mentioned in the (Subscribing private voice chats updates)[#Subscribing-private-voice-chats-updates] section with the connection URL as well.
Upon receiving the connection URL, the client must connect to the LiveKit room using any LiveKit's client implementation. To communicate via voice, users must publish audio tracks in the room and process the audio tracks for which they're already subscribed. Check the LiveKit's documentation on how to do this.
Here's the interaction flow of the Explorer and the Social Service:
sequenceDiagram
actor U2 as Callee
participant SS as Social Server
participant LK as LiveKit
U2->>SS: Accept private voice chat using the id
alt The private voice chat doesn't exist
SS-->>U2: Not found error
else The private voice chat exists
SS-->>U2: Connection URL
end
U2->>LK: Connect to the room
The RPC call is specified in the protocol as:
message AcceptPrivateVoiceChatPayload {
string call_id = 1;
}
message AcceptPrivateVoiceChatResponse {
message Ok {
string call_id = 1;
PrivateVoiceChatCredentials credentials = 2;
}
oneof response {
Ok ok = 1;
InternalServerError internal_server_error = 2;
NotFoundError not_found = 4;
ForbiddenError forbidden_error = 5;
}
}
rpc AcceptPrivateVoiceChat(AcceptPrivateVoiceChatPayload) returns (AcceptPrivateVoiceChatResponse) {}
Ending a private voice chat can be done through the Social Service in any of their states (ringing or on going) or through LiveKit if the user is already on an on going private voice chat.
Ending a private voice chat is done by performing the EndPrivateVoiceChat
RPC
call to the Social Service with the call id of the voice chat. A voice chat can be ended at
any state by any of the participants, but the callee should use the reject operation if they
want to reject a private voice chat. This process will result in the caller being notified
through the subscription's updates mechanism mentioned in the
Subscribing private voice chats updates
section with the call id that is being terminated.
This RPC call can be used in two different states of a private voice chat:
For each of these states the client must react to them in the following way:
Here's the interaction flow of the Explorer and the Social Service:
sequenceDiagram
actor U1 as Caller or calle
participant SS as Social Server
participant LK as LiveKit
U1->>SS: End private voice chat with id
break Private voice chat exists
SS-->>U1: Ok
end
LK->>U1: Room destroyed
message EndPrivateVoiceChatPayload {
string call_id = 1;
}
message EndPrivateVoiceChatResponse {
message Ok {
string call_id = 1;
}
oneof response {
Ok ok = 1;
InternalServerError internal_server_error = 2;
NotFoundError not_found = 3;
}
}
rpc EndPrivateVoiceChat(EndPrivateVoiceChatPayload) returns (EndPrivateVoiceChatResponse) {}
An on going LiveKit private voice chat can be ended by issuing a voluntarily disconnection from the room. This action is monitored by our services and will result in the LiveKit room being destroyed.
The client must be listening to the LiveKit room events to react upon disconnection. In this case, the reason for the disconnection will be set as the destruction of the room, which the client must treat as the end of the private voice chat.
At least for now, we can't know if a user is connected or not to the Social Service;
therefore, we can't strictly enforce the requirement to prevent users from starting private
voice chats with disconnected users. To circumvent this issue, the Explorer must request the
client for any incoming private voice chats the user has when connecting to the Social
Service. This call is done through the GetIncomingPrivateVoiceChatRequest
RPC
call.
Here's the interaction flow of the Explorer and the Social Service:
sequenceDiagram
actor U1 as Callee
participant SS as Social Service
U1->>SS: Get incoming private voice chat
alt There is an incoming private voice chat
SS-->>U1: Private voice chat id
else There isn't an incoming private voice chat
SS-->>U1: Not found error
end
The RPC call is specified in the protocol as:
message GetIncomingPrivateVoiceChatRequestResponse {
message Ok {
User caller = 1;
string call_id = 2;
}
oneof response {
Ok ok = 1;
NotFoundError not_found = 2;
InternalServerError internal_server_error = 3;
}
}
rpc GetIncomingPrivateVoiceChatRequest(google.protobuf.Empty) returns (GetIncomingPrivateVoiceChatRequestResponse) {}
Subscribing to the private voice chats updates stream is critical for the voice chat flow to
work. This subscription is done by performing the
SubscribeToPrivateVoiceChatUpdates
RPC call to the Social Service. Both the
callee and the caller need to be subscribed. We recommend this subscription to be done upon
connecting to the client.
Here's the interaction flow of the Explorer and the Social Service:
sequenceDiagram
actor U1 as Callee
actor U2 as Caller
participant SS as Social Service
U1->>SS: Subscribe to private voice updates
U2-->>SS: Subscribe to private voice updates
alt Start private voice chat published
SS-->>U1: Notify voice chat started with id and caller address
else Private voice chat accepted published
SS-->>U2: Notify voice chat accepted with credentials
else Private voice chat rejected published
SS-->>U2: Notify voice chat rejected with id
else Private voice chat ended published
alt Voice chat ended due to disconnection
SS-->>U1: Notify voice chat ended with id
SS-->>U2: Notify voice chat ended with id
else Voice chat ended by callee
SS-->>U2: Notify voice chat ended with id
else Voice chat ended by caller
SS-->>U1: Notify voice chat ended with id
end
else Private voice chat expired published
SS-->>U1: Notify voice chat expired with id
SS-->>U2: Notify voice chat expired with id
end
Each update implies performing a different action, here is an explanation on how to act upon receiving them:
The RPC call is specified in the protocol as:
message PrivateVoiceChatUpdate {
string call_id = 1;
PrivateVoiceChatStatus status = 2;
optional User caller = 3;
optional User callee = 4;
optional PrivateVoiceChatCredentials credentials = 5;
}
rpc SubscribeToPrivateVoiceChatUpdates(google.protobuf.Empty) returns (stream PrivateVoiceChatUpdate) {}
Any user connected to a LiveKit room can experience an abrupt disconnection due to connectivity issues. Our services will be monitoring the connection and disconnection of users from rooms. If a user has been abruptly disconnected from the room and wasn't able to reconnect for a period of time, our service will issue the room as destroyed. Any users in the room will be disconnected with the room destroyed reason, which they must handle and act upon it, ending the voice chat.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.