MSC2746 extends the Matrix Voice over IP functionality with more reliability, hold/resume and DTMF. The ability to transfer a call to another destination is absent from the current Matrix VoIP spec and is not covered by MSC2746.
Adding this will allow for scenarios such as:
- A customer service agent receiving a call using a Matrix client, then transferring the customer to another department in the company.
- A personal assistant or switchboard operator calling another party on behalf of a user, then connecting the user directly to their destination.
This MSC builds on MSC2746, making
use of the invitee
field on m.call.invite
in particular.
Throughout this MSC, industry standard nomenclature is used to refer to parties involved in the call transfer:
- Transferee: The party who is being transferred
- Transferor: The party initiating the transfer.
- Transfer target: The party that the transferee is being transferred to.
This proposal introduces the m.call.replaces
event which signals the intent of a
participant in a call to replace the call with another, such that the other participant
ends up in a call with a new user. This should appear as one, seamless call to the user
being transferred, with the possible exception of a permission prompt and some UI to
indicate that they are being transferred.
An m.call.replaces
event has fields:
call_id
: The ID of the call that the transferor intends to replaceparty_id
: The transferor's client's party ID for the call that it intends to replace.replacement_id
: An identifier for the call replacement itself, generated by the transferor.target_room
: Optional. If specified, the transferee client waits for an invite to this room and joins it (possibly waiting for user confirmation) and then continues the transfer in this room. If absent, the transferee contacts the Matrix User ID given in thetarget_user
field in a room of its choosing.target_user
: An object giving information about the transfer target:id
: The matrix user ID of the transfer targetdisplay_name
: (Optional) The display name of the transfer target.avatar_url
: (Optional) The avatar URL of the transfer target.
create_call
: If specified, gives the call ID for the transferee's client to use when placing the replacement call. Mutually exclusive withawait_call
.await_call
: If specified, gives the call ID that the transferee's client should wait for. Mutually exclusive withcreate_call
.
The display name and avatar URL of the transfer target in the target_user
field
are purely informational and given by the transferor, so should be treated as such for
trust purposes. They should be omitted if the target has no display name or avatar URL set,
respectively. It is recommended that the transferor uses the transfer target's global
display name and avatar URL, or potentially those from the target room if available,
rather than details from a direct message with the transfer target: the display name and
avatar URL in the direct message room should be treated as private.
From the transferor's point of view, a call transfer starts when they are in active calls with both the transferee and the transfer target. One or both calls could be on hold and the call with the transfer target may have not yet been answered (a 'blind transfer').
It also introduces an event to reject the transfer, m.call.reject_replacement
, which has
fields:
call_id
: The ID of the call that was intended to be replacedparty_id
: The party ID of the client rejecting the replacementreplacement_id
: The replacement ID of the replacement that is being rejectedreason
: The reason a replacement is being rejected. One of:declined
: Either the user has declined the transfer, or the client has done so on their behalf (eg. due to a policy set in their client).failed_room_invite
: The transferee's client timed out whilst waiting for the room invite to arrivefailed_call_invite
: The transferee's client timed out whilst waiting for the invite for the replacement call to arrive.failed_call
: The replacement call itself could not be made. Thecall_failure_reason
field may be used to give the reason the replacement call failed.
call_failure_reason
: (Optional) May be present ifreason
isfailed_call
, in which case it gives thereason
field from the replacement call's hangup event.
To initiate a call transfer, the transferor's client:
- Attempts to find a suitable room. This should be a room that contains at least all three users (and generally no others unless there is a specific reason to use a certain room).
- If a suitable room cannot be found, it should create one, but it should not yet invite the users, otherwise the transferee will receive the room invite before they receive the call replace event.
- Once it has created a new room or found an existing one, it then sends two
m.call.replace
events. One to the room for its call with the transfer target and one to the room for its call with the transferee, each giving user information for the other and with thecall_id
field set to the call ID of the respective call. Thetarget_room
field is the newly created or chosen room in both cases. The transferor generates a new call ID and puts this call ID in thecreate_call
field in one replace event and in theawait_call
field of the other. These can be either way around although it is suggested that the transferee is instructed to create the new call. - Once each event has been sent to each user, it can invite the corresponding user to the target room (or may choose to wait for both replace events to send and invite both users with a single API call).
- Additionally, once each replace event has been sent, it may choose to end the respective call, although it would generally wait for the other parties to end them unless it is explicitly intending to perform a blind transfer.
- The client may monitor the target room to observe the progress of the replacement call being established.
Upon receving an m.call.replaces
event, a client behaves as follows:
- Checks that it is currently active in a call with call ID given in the
call_id
field, that the other party in the call matches the sender of the replaces event and that signalling for the call is being exchanged in the same room as the replaces event. If any of these are not the case, the client ignores the event. - Makes a decision on whether to act on the call transfer. How the client makes this decision is not defined in this MSC. A client may, for example, wish to trust any user on specific homeservers or in specific rooms or communities to transfer the user, or it may wish to prompt the user, bearing in mind the display name and avatar of the transfer target supplied by the transferor could be falsified.
- Once it has decided to act on the call transfer, it should continue to show the original call as active (or represented in a 'transferring state') in the UI, even if the original call is hung up. It continues to do so until the original call has either been replaced by the new call or the replacement has failed.
- If the replace event has a
target_room
specified and the user is not already in the specified room, it waits for an invite to that room to arrive, then accepts the invite. Once in the room, if them.call.replaces
event hadcreate_call
, it sends anm.call.invite
in the target room, setting thecall_id
to the value of thecreate_call
field and theinvitee
field to theid
field oftarget_user
. If the replaces event containedawait_call
, the client waits for a call with ID equal to that in theawait_call
field. It is up to the transferee's client to decide how long to wait for each invite before timing out. If it times out, it sends anm.call.reject_replacement
event in the original room to signal that the replcaement has failed. - If this call is sucessfully answred by the invitee, the client sends a hangup event in the room for the original call, ending the call.
The m.call.reject_replacement
is sent if the client does not accept the call transfer (eg.
it decides that the transferor is not sufficiently trustworthy, or it prompted the user and the
user chose to reject the transfer). The event has replacement_id
equal to the replacement_id
of the m.call.replaces
event that initiated the transfer.
On receiving this, the transferor aborts the transfer process and informs the transferor user that the call transfer was rejected, and by which party. There is no explicit event to accept the transfer.
This proposal also introduces a field on m.call.invite
and m.call.answer
events at the top
level with the key capabilities
, whose value is an object. We define the key,
m.call.transferee
which, if set to true, states that the sender of the event supports the
m.call.replaces
event and therefore supports being transferred to another destination.
For example:
{
"type": "m.call.invite",
"room_id": "!rO0m_1d:example.org",
"content": {
"call_id": "123456",
"lifetime": 60000,
"capabilities": {
"m.call.transferee": true,
},
"offer": {
"type": "offer",
"sdp": [...],
},
"version": 1,
},
}
If this key is absent or set to anything other than the boolean, true
, or if
the capabilities
object is missing altogether, it should be assumed that the
sender of the invite or answer does not support call transfers and clients should
reflect this in the UI accordingly.
We also define a capability called m.call.dtmf
. Clients should only display UI for sending
DTMF during a call if the other party advertises this capability (boolean value true
).
A call transfer is fairly complex and involves a lot of round-trips and state on clients, and
is fairly complex for clients to implement, in comparison to the rest of the VoIP spec which
is reasonably lightweight. If there were a PBX or soft switch on the path, this may potentially
handle the logic of doing the actual transfer meaning that the transferor would just need to
send a n m.call.replaces
event to initiate the transfer, and clients would not have to
implement the rest of the protocol for being transferred if their leg of the call remained with
the PBX / soft switch.
No provision is made for a transferor to prompt a transferee to place a call to a transfer target without there being an existing active call between the transferor and the transferee. SIP does have this capability using the REFER method. This would require a mechanism for the transferor to identify the transferee's individual devices, akin to a GRUU in SIP, and be able to direct a specfic one of them to place the call.
Equivalently, this could be achieved in a different way, for example, all the transferee's devices could ring, and when they 'answer' on one of them, it places the call to the transfer target. Similar behaviour can be achieved with the mechanisms described by this MSC, apart from the fact that the initial incoming call to the transferee would be, and would appear as, a normal incoming call from the transferor rather than being presented as a call to the transfer target.
Consideration was given to using a more generic event to refer conversations in general
between rooms as well as calls given the overlap in functionality. With threading support,
this could also transparently move threads between rooms. However, there are a number of
specific semantics associated with transferring calls specifically, and m.call.replace
better captures the behaviour of replacing the current call with a new one, so this MSC
opts to use a specific event for transferring calls.
The target_user
field of the m.call.replaces
event could be fabricated by the transferor,
as mentioned above. The transferee's client would have to present it to the user in this context.
It would be up to clients to decide when to honour an incoming transfer request. If they accepted
any instruction to transfer the call, it would be possible to cause a user to place a VoIP call
to any Matrix user just by establishing a call to them and sending an m.call.replaces
event.