---
sidebar_position: 6
sidebar_label: Calls
---

# Calls

Calls between two people and group calls, in `WordPlusRealtimeCalls`, on the same relay and media servers as browsers: a phone and a browser call each other, and either may add someone. The Better Messages call screen, in `WordPlusRealtimeUI`, draws them with no screen code of yours. Both products are optional.

- **Calls between two people** ring on every page and app the callee has open, and run directly between the two, through TURN when a network needs it, or in a room on the WordPlus Cloud media servers when nothing else works.
- **Group calls** run on the WordPlus Cloud media servers, with no limit on people in a call. Up to 32 people on video at once for each 1,000 connections take no credits, a person on audio counting half, and each one above takes a credit a minute from your account.
- **Encrypted end to end**, with keys only your server gives out ([what WordPlus Cloud sees](../apps/calls.md#what-wordplus-cloud-sees)).
- **Calls ring while the app is open.** Ringing a closed app needs push, which this SDK leaves for later.
- **User ids are integers** in this version, as in the relay. The SDK takes them as strings (`"34"`).

## Who may call

Your server declares calls by pattern, as it declares rooms ([calls in depth](../apps/calls.md#who-may-call)):

**Node**

```ts
realtime.calls( 'support:{a}:{b}', ( user, withUser, { a, b } ) => [ a, b ].map( Number ).includes( user ) && [ a, b ].map( Number ).includes( withUser ) );
realtime.groupCalls( 'meeting:{id}', ( user ) => isStaff( user ), { encrypt: true } );
```

**PHP**

```php
$realtime->calls( 'support:{a}:{b}', fn ( $user, $with, array $p ) => ! array_diff( [ $user, $with ], [ (int) $p['a'], (int) $p['b'] ] ) );
$realtime->groupCalls( 'meeting:{id}', fn ( $user ) => is_staff( $user ), [ 'encrypt' => true ] );
```

**Python**

```python
realtime.calls("support:{a}:{b}", lambda user, with_user, p: {user, with_user} <= {int(p["a"]), int(p["b"])})
realtime.group_calls("meeting:{id}", lambda user, p: is_staff(user), encrypt=True)
```

The app asks your calls route (`{endpoint}/calls`) with its headers, as the browser does.

## The call screen

```swift
import WordPlusRealtimeCalls
import WordPlusRealtimeUI

CallScreen.shared.attach(rt.calls)   // rings for every call coming in, and shows it once answered
CallScreen.shared.onAnswered = { answered in
    switch answered {
    case let .call(call): follow(call)          // the call a ring became
    case let .group(group): follow(group)       // or the group call it rang into
    }
}

let call = try await rt.calls.start("support:34:42", to: "34", type: .video)
CallScreen.shared.show(call, name: "Ann", avatar: "https://shop.example.com/ann.jpg")

let meeting = try await rt.calls.group("meeting:daily", type: .video)
CallScreen.shared.show(meeting, title: "Daily meeting")
```

- **It is the Better Messages screen itself,** the same files as its app: the ring with Answer and Decline, the call between two with the picture of the other side and your own in the corner, the grid of a group call, and the controls (microphone, camera, turning the camera round, the speaker, picture in picture, hanging up).
- **It sits in a window of its own** above the app. Its chevron puts it away and the call goes on: show a bar of your own meanwhile, and `CallScreen.shared.restore()` brings it back. `hide()` puts it away from your code.
- **A call ringing during another** is answered busy.
- **A call that becomes a group call** stays on the screen, as the group call.
- **Its words and colours:** `configure(.init(strings: [...], tokens: [...], tokensDark: [...], darkMode: false))`, the strings by the Better Messages keys (`answer`, `reject`, `end_call`, `calling`, `connecting` …), the colours by its design tokens (`"--bm-color-accent": "#21759b"`). The English is the default for any string left out.
- `CallScreen.shared.log` hands you the lines the screen writes, for your own log.

## Calls of your own

The same calls, for screens you draw:

```swift
rt.calls.onIncoming { incoming in
    // incoming.from, incoming.info ["name", "avatar"], incoming.type, incoming.group (a ring into a group call)
    incoming.onOver { reason in hideRing() }                   // answered elsewhere, the caller gave up …
    Task { @MainActor in
        switch try await incoming.accept() {                   // or try await incoming.decline(busy: true)
        case let .call(call): show(call)
        case let .group(group): show(group)
        }
    }
}

let call = try await rt.calls.start("support:34:42", to: "34", type: .video)
call.onState { state in label.text = state.rawValue }        // ringing, connecting, connected, reconnecting, ended
call.onRemote { remoteView.show(call.remoteCamera) }          // what the other side sends changed
call.onEnded { reason in close(reason) }                      // hangup, declined, busy, cancelled, group …

let remoteView = CallVideoView()                              // a UIView
let ownView = CallVideoView()
ownView.mirrored = true
ownView.show(call.localCamera)

call.microphone = false                                       // mute
call.camera = false                                           // stop sending the camera
call.flipCamera()                                             // the front one to the back, and back
call.end()                                                    // hangs up, or calls it off while it rings
```

- **The microphone and, for a video call, the camera** open as the call begins, so iOS asks the user then ([shipping](shipping.md#microphone-and-camera)).
- **`call.path`** says what carries it: `.direct`, `.relay` (TURN) or `.room`. `call.peer` and `call.peerInfo` who the other side is.
- **`rt.calls.stopRinging()`** lets go of this app line: its user calls no longer ring here.

## Adding people

Once a call is up, either side may ring someone else into it. Both sides go on in a group call of their own, without hanging up:

```swift
call.onGroup { group in follow(group) }                // both sides, and the call then ends as `group`
let invite = try await call.add("43")                   // rings user 43, throws its code
```

- **Your rule decides,** as for any call: the user calling the person about the call name, then the person taking a call from them.
- **The person rung in** gets an `IncomingCall` whose `group` is set, and `accept()` answers `.group`.
- **`group.add(_:)`** rings more people in, from anyone in it. A group call your server names (`rt.calls.group`) takes nobody that way: `not_a_call`.

## Group calls

```swift
let group = try await rt.calls.group("meeting:daily", type: .video)
group.microphone = true                       // nothing is sent into a group call until asked
group.camera = true
group.onPeople { people = group.people }      // [GroupPerson]: user, info, camera, microphone, speaking
group.onState { state in label.text = state.rawValue }
group.onEnded { reason in close(reason) }
group.leave()
```

- **`group.encrypted`** says whether its media is encrypted end to end: always for the group call that comes from a call, and for one your server names when its rule says `encrypt`.
- **In this version** a group call video is drawn by the call screen. Drawing it in screens of your own comes later. Audio, `people` and every control work without it.

## Why a call ended

`call.endReason`, and the reason `onEnded` gets: the same codes as the browser ([why a call ended](../apps/calls.md#why-a-call-ended)), plus `connection_failed` when no path connected. A call that could not start throws a `CallError` with the code: `forbidden`, `no_user`, `not_included`, `auth_failed` ([errors](../apps/errors.md#calls)).
