207 lines
12 KiB
Markdown
207 lines
12 KiB
Markdown
# MSC2747: Transferring VoIP Calls
|
|
|
|
[MSC2746](https://github.com/matrix-org/matrix-doc/pull/2746) 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](https://github.com/matrix-org/matrix-doc/pull/2746), making
|
|
use of the `invitee` field on `m.call.invite` in particular.
|
|
|
|
## Nomenclature
|
|
|
|
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.
|
|
|
|
## Proposal
|
|
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 replace
|
|
* `party_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 the `target_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 target
|
|
* `display_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 with `await_call`.
|
|
* `await_call`: If specified, gives the call ID that the transferee's client should wait
|
|
for. Mutually exclusive with `create_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 replaced
|
|
* `party_id`: The party ID of the client rejecting the replacement
|
|
* `replacement_id`: The replacement ID of the replacement that is being rejected
|
|
* `reason`: 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 arrive
|
|
* `failed_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. The `call_failure_reason`
|
|
field may be used to give the reason the replacement call failed.
|
|
* `call_failure_reason`: (Optional) May be present if `reason` is `failed_call`, in which
|
|
case it gives the `reason` 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 the
|
|
`call_id` field set to the call ID of the respective call. The `target_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 the `create_call` field in one replace event and in the `await_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 the `m.call.replaces` event had `create_call`, it sends an `m.call.invite` in the target room,
|
|
setting the `call_id` to the value of the `create_call` field and the `invitee` field to the
|
|
`id` field of `target_user`. If the replaces event contained `await_call`, the client waits
|
|
for a call with ID equal to that in the `await_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 an
|
|
`m.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.
|
|
|
|
### Capability Advertisment
|
|
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`).
|
|
|
|
## Potential issues
|
|
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.
|
|
|
|
## Alternatives
|
|
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.
|
|
|
|
## Security considerations
|
|
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.
|