diff --git a/proposals/2747-voip-call-transfer.md b/proposals/2747-voip-call-transfer.md new file mode 100644 index 00000000000..e7431a5dce5 --- /dev/null +++ b/proposals/2747-voip-call-transfer.md @@ -0,0 +1,206 @@ +# 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.