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

# Calls

Calls between two people and group calls, in `cloud.wordplus:realtime-calls`, 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 `cloud.wordplus:realtime-ui`, draws them with no screen code of yours. Both libraries 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

```kotlin
import cloud.wordplus.realtime.calls.calls
import cloud.wordplus.realtime.ui.CallScreen

val calls = rt.calls(context)
CallScreen.attach(context, calls)   // rings for every call coming in, and shows it once answered
CallScreen.onAnswered = { answered ->
    when (answered) {
        is IncomingCall.Answered.ToCall -> follow(answered.call)       // the call a ring became
        is IncomingCall.Answered.ToGroup -> follow(answered.group)     // or the group call it rang into
    }
}

viewModelScope.launch {
    val call = calls.start("support:34:42", "34", CallType.VIDEO)
    CallScreen.show(context, call, name = "Ann")

    val meeting = calls.group("meeting:daily", CallType.VIDEO)
    CallScreen.show(context, 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 and audio route, picture in picture, hanging up).
- **It is an activity of its own** over the app, which the library declares. Its chevron puts it away and the call goes on, as `CallScreen.hide()` does from a button of your own: show a bar of your own meanwhile (`CallScreen.active` says a call is on), and `CallScreen.restore()` brings it back.
- **The microphone and camera:** it asks for them when a call is answered, if the app has not been allowed them yet.
- **A call ringing during another** is answered busy.
- **A call that becomes a group call** comes back on the screen, as the group call.
- **Its words and colours:** `CallScreen.configure(CallScreen.Appearance(strings = mapOf(…), tokens = mapOf(…), tokensDark = mapOf(…), darkMode = false))`, the strings by the Better Messages keys (`answer`, `reject`, `end_call`, `calling` …), the colours by its design tokens (`"--bm-color-accent" to "#21759b"`). The English is the default for any string left out.
- `CallScreen.log` hands you the lines the screen writes, for your own log.

## Calls of your own

The same calls, for screens you draw:

```kotlin
calls.onIncoming { incoming ->
    // incoming.from, incoming.info (name, avatar), incoming.type, incoming.group (a ring into a group call)
    incoming.onOver { reason -> hideRing() }                       // answered elsewhere, the caller gave up …
    viewModelScope.launch {
        when (val answered = incoming.accept()) {                  // or incoming.decline(busy = true)
            is IncomingCall.Answered.ToCall -> show(answered.call)
            is IncomingCall.Answered.ToGroup -> show(answered.group)
        }
    }
}

val call = calls.start("support:34:42", "34", CallType.VIDEO)
call.onState { state -> label = state.wire }                      // ringing, connecting, connected, reconnecting, ended
call.onRemote { remoteView.show(call.remoteCamera) }               // what the other side sends changed
call.onEnded { reason -> close(reason) }                           // hangup, declined, busy, cancelled, group …

val remoteView = CallVideoView(context)                            // a View
val ownView = CallVideoView(context).apply { mirrored = call.frontCamera }
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
remoteView.release()                                               // when the view goes
```

- **Ask for `RECORD_AUDIO` and, for video, `CAMERA`** before a call, as for any screen of your own. The library manifest declares them. One allowed only after the call began: call `call.permissionsGranted()`, and the call opens it then.
- **`call.path`** says what carries it: `DIRECT`, `RELAY` (TURN) or `ROOM`. `call.peer` and `call.peerInfo` who the other side is.
- **`calls.stopRinging()`** lets go of this app line: its user calls no longer ring here.
- `start`, `group`, `accept` and `add` switch to the main thread themselves. Call them from any coroutine.

## 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:

```kotlin
call.onGroup { group -> follow(group) }        // both sides, and the call then ends as `group`
val invite = 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 `ToGroup`.
- **`group.add(user)`** rings more people in, from anyone in it. A group call your server names (`calls.group`) takes nobody that way: `not_a_call`.

## Group calls

```kotlin
val group = calls.group("meeting:daily", CallType.VIDEO)
group.microphone = true                         // nothing is sent into a group call until asked
group.camera = true
group.onPeople { people = group.people }        // List<GroupPerson>: user, info, camera, microphone, speaking
group.onState { state -> label = state.wire }
group.onEnded { reason -> 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 `CallException` with the code: `forbidden`, `no_user`, `not_included`, `auth_failed` ([errors](../apps/errors.md#calls)).
