Hand a call to another of your devices #23

Open
opened 2026-09-12 00:45:07 +00:00 by paulhitt · 2 comments
paulhitt commented 2026-09-12 00:45:07 +00:00 (Migrated from gitlab.com)

On a call on the phone, walk to the desk, tap "Move to iPad", and keep talking. The other person hears a short gap and nothing else.

Apple can do this inside FaceTime. No other messenger does it at all.

How it works

The account's own devices already have a private channel during a call: the device that answers tells the others to stop ringing. Handoff is that channel carrying more.

  1. The device on the call offers it to this account's other devices, naming the call and the peer. Only idle devices offer to take it.
  2. The taking device builds a fresh connection and sends the peer an offer under the same call id.
  3. The peer treats an offer for a call it is already on as a renegotiation, answers it, and never leaves the call screen. One call is logged, not two.
  4. The taking device says it has the call; the original tears down its connection and sends the peer nothing.

Rules this encodes

  • The peer is never hung up on. No end is sent at any point during a handoff, or the far end drops the call and redials.
  • If nothing takes the call within a few seconds, the call stays where it is and the offer is withdrawn. A handoff that half happens must leave the original working.
  • Only this account's own devices are offered the call, by the same routing check the stop-ringing path already makes.
  • A device that does not understand the offer ignores it, which is correct: it does not offer to take the call.
  • The relay learns nothing new. This rides the ratchet like every other call signal.

Worth deciding first

  • Whether the peer's build must be new enough to accept a renegotiating offer. If it is not, the handoff should not start rather than dropping the call to find out.
  • Whether video hands off in the first version, or voice only.

Pairs with flamenet-messenger-ios#74.

On a call on the phone, walk to the desk, tap "Move to iPad", and keep talking. The other person hears a short gap and nothing else. Apple can do this inside FaceTime. No other messenger does it at all. ## How it works The account's own devices already have a private channel during a call: the device that answers tells the others to stop ringing. Handoff is that channel carrying more. 1. The device on the call offers it to this account's other devices, naming the call and the peer. Only idle devices offer to take it. 2. The taking device builds a fresh connection and sends the peer an offer under the same call id. 3. The peer treats an offer for a call it is already on as a renegotiation, answers it, and never leaves the call screen. One call is logged, not two. 4. The taking device says it has the call; the original tears down its connection and sends the peer nothing. ## Rules this encodes - The peer is never hung up on. No end is sent at any point during a handoff, or the far end drops the call and redials. - If nothing takes the call within a few seconds, the call stays where it is and the offer is withdrawn. A handoff that half happens must leave the original working. - Only this account's own devices are offered the call, by the same routing check the stop-ringing path already makes. - A device that does not understand the offer ignores it, which is correct: it does not offer to take the call. - The relay learns nothing new. This rides the ratchet like every other call signal. ## Worth deciding first - Whether the peer's build must be new enough to accept a renegotiating offer. If it is not, the handoff should not start rather than dropping the call to find out. - Whether video hands off in the first version, or voice only. Pairs with flamenet-messenger-ios#74.
paulhitt commented 2026-09-12 00:46:11 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
paulhitt commented 2026-09-12 02:11:43 +00:00 (Migrated from gitlab.com)

Decision on the peer's build, taken 2026-09-11: refuse to start the handoff rather than try and recover.

A handoff needs the other side to accept a second offer carrying the id of a call it is already on, and treat it as renegotiation instead of a new ring. Today it does the opposite: the offer branch answers busy to anything arriving while a call is up. An older build will keep doing that.

So the moving device asks its own devices first, and hands the call over only once one of them has taken it and the peer has answered the renegotiating offer. If the peer cannot, the call stays exactly where it is and the interface says it could not be moved. A handoff that cannot work must never disturb a call that is working.

Three things the reading turned up that the implementation has to respect:

  • the offer branch refuses anything while a call is up; matching call.id against the current call is what separates a handoff from a second caller
  • the answer branch drops an answer whose call.id is not the current call, so the original device has to let go before the taking device's answer can land
  • tearing down with notifyPeer = true sends call-end to the peer, which would drop the very call being handed over. The original device tears down quietly.

Parked for now: anonymous polls come first.

Decision on the peer's build, taken 2026-09-11: **refuse to start the handoff rather than try and recover.** A handoff needs the other side to accept a second offer carrying the id of a call it is already on, and treat it as renegotiation instead of a new ring. Today it does the opposite: the offer branch answers `busy` to anything arriving while a call is up. An older build will keep doing that. So the moving device asks its own devices first, and hands the call over only once one of them has taken it and the peer has answered the renegotiating offer. If the peer cannot, the call stays exactly where it is and the interface says it could not be moved. A handoff that cannot work must never disturb a call that is working. Three things the reading turned up that the implementation has to respect: - the offer branch refuses anything while a call is up; matching `call.id` against the current call is what separates a handoff from a second caller - the answer branch drops an answer whose `call.id` is not the current call, so the original device has to let go before the taking device's answer can land - tearing down with `notifyPeer = true` sends `call-end` to the peer, which would drop the very call being handed over. The original device tears down quietly. Parked for now: anonymous polls come first.
Sign in to join this conversation.