ADR-284: Private voice chat

More details about this document
Latest published version:
https://adr.decentraland.org/adr/ADR-284
Authors:
LautaroPetaccio
pentreathm
aleortega
Feedback:
GitHub decentraland/adr (pull requests, new issue, open issues)
Edit this documentation:
GitHub View commits View commits on githistory.xyz

Abstract

This specification defines the private voice chat protocol, designed to provide voice communication between two users in the Explorer, simulating a two person call.

Context

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).

Specification

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

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

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

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

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 through the Social Service

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:

  1. The voice chat is in the ringing state.
  2. The user is in an on going private voice chat (connected to LiveKit).

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) {}

Ending a private voice chat through LiveKit

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.

Getting incoming private voice chats

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 private voice chats updates

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) {}

Recovering from a disconnection

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.

RFC 2119 and RFC 8174

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.

License

Copyright and related rights waived via CC0-1.0. Review