---
sidebar_position: 6
---

# Calls between two people

A call rings on every page the other person has open, and runs straight between the two browsers once answered:

1. **Direct:** the two browsers connect to each other, which is the best picture and sound, at no cost.
2. **Relayed:** when a network allows no direct connection, the media goes through WordPlus Cloud's TURN servers, over UDP or over TLS on port 443.
3. **A room:** when neither works, the call moves to a room on WordPlus Cloud's media servers, on its own and without hanging up.

The [call screen](call-screen.md) does all of this with two HTML elements. This page is for building your own interface.

## Who may call whom

Your plugin owns the calls whose names start with its prefix, and decides each time someone places or answers one:

```php
add_action( 'plugins_loaded', function () {
	wordplus_realtime()->calls( 'support-', function ( $user_id, $call, $with ) {
		if ( null === $with ) {
			return false;                    // a group call's room: see group-calls.md
		}

		$agent = my_support_agent_id();

		if ( $user_id === $agent ) {
			return 'support-' . $with === $call;                    // the agent answers a customer's call
		}

		return $with === $agent && 'support-' . $user_id === $call;   // a customer calls the agent, on their own call
	} );
} );
```

- **`$user_id`** places or answers the call: the signed-in user, or a visitor's id ([visitors](#visitors)).
- **`$call`** is the call's name, such as `support-42`, which your plugin chooses. Names follow the channels' rules: up to 200 of `A-Z a-z 0-9 _ - = @ , . ;`.
- **`$with`** is the other person: the one being called when `$user_id` places the call, the caller when `$user_id` answers. It is `null` for a [group call](group-calls.md).
- **The answer** is `false`, `true`, or an array whose `info` is what the other side sees of `$user_id`. By default that's their display name and picture: `array( 'name' => …, 'avatar' => … )`.
- **Both sides ask.** The caller's page asks when placing the call, and the callee's when answering, so your callback decides for both.
- **The longest prefix wins,** as for channels.

The site needs a plan that includes calls; without it a call ends at once, and a page's line is refused with `not_included`.

## The browser's side

Load the calls script on the pages that place or answer calls:

```php
wp_enqueue_script( 'my-support', plugins_url( 'support.js', __FILE__ ), array( wordplus_realtime()->calls_script() ), '1.0.0', true );
```

```js
// The page's calls, shared with the call screen and every other plugin on the page. Null without the site's settings.
const page = window.wordplusRealtimeCalls && wordplusRealtimeCalls.page();

if ( page ) {
	const { calls } = page;
	// …
}
```

`page()` makes the page's channels and calls once, from `window.wordplusRealtimeConfig`. Use it rather than `wordplusRealtimeCalls.calls( rt, config )`, so a page holds one line and a call rings once.

## Placing a call

```js
let call;

try {
	call = await calls.start( 'support-42', { to: 34, type: 'video' } );   // type: 'video' or 'audio'
} catch ( e ) {
	console.warn( e.code );   // forbidden: your callback said no; no_user; auth_failed …
}

call.on( 'state', ( state ) => showState( state ) );   // ringing, connecting, connected, reconnecting, ended
call.on( 'remote', ( remote ) => {
	if ( remote.camera ) remote.camera.attach( document.querySelector( '#their-video' ) );
	if ( remote.microphone ) remote.microphone.attach( document.querySelector( '#their-audio' ) );
} );
call.on( 'ended', ( reason ) => showEnded( reason ) );

const media = await navigator.mediaDevices.getUserMedia( { audio: true, video: true } );
call.setTrack( 'microphone', media.getAudioTracks()[ 0 ] );
call.setTrack( 'camera', media.getVideoTracks()[ 0 ] );
```

Starting the call makes it ring on every page of user 34 that holds their line. It rings for 45 seconds, then ends with `no_answer`.

## Ringing and answering

Every page that loads the calls script for a signed-in user holds that user's line, so their calls ring there:

```js
calls.on( 'incoming', ( incoming ) => {
	// incoming.call: the call's name; incoming.from: the caller's id; incoming.info: what your callback gave for them;
	// incoming.type: 'video' or 'audio'.
	showRing( incoming );

	incoming.on( 'over', ( reason ) => hideRing() );   // answered_elsewhere, rejected, busy, cancelled, no_answer
} );

// The person answers:
const call = await incoming.accept();   // the call, connecting; rejects with `forbidden` when your callback says no
// Or turns it down:
await incoming.decline();               // the caller hears `rejected`; decline( 'busy' ) and they hear `busy`
```

- **A call rings on every page of the person** at once, on every device. It stops everywhere as soon as they answer or decline on one, with `over`.
- **A page opened while a call rings** rings too.
- **`calls.incoming()`** lists the calls ringing now.
- **A page that shouldn't ring** passes `ring: false` in its settings, or calls `calls.stopRinging()`.

## The call

| Member | |
|---|---|
| `id` | this attempt at the call, made by the caller's page |
| `name`, `type`, `role` | the call's name, `video` or `audio`, and `caller` or `callee` |
| `peer` | `{ user, info }`: the other person |
| `state` | `ringing` (the caller, until answered), `connecting`, `connected`, `reconnecting`, `ended` |
| `path` | `direct`, `relay` or `room`: what carries it now |
| `remote` | the other side's tracks, `camera`, `microphone`, `screen` and `screenAudio`, each with `attach( element )` and `detach()`, `media` (what they have switched on) and `quality` (`excellent`, `good`, `poor`, `lost`) |
| `endReason` | why it ended |
| `setTrack( source, track )` | what this page sends from `camera`, `microphone`, `screen_share` or `screen_share_audio`; `null` stops sending it |
| `end()` | hangs up, or calls it off while it still rings (`cancelled`) |

Events: `state`, `path`, `remote`, `reacquire` (the other side stopped hearing this page's microphone or camera: open the device again), `autoplay_blocked` (the browser needs a click before it plays the sound), and `ended`.

**Mute** by sending `null` for the microphone and the track again to unmute. **Turn the camera off** the same way, and stop the track so the camera's light goes off. **Share a screen** with `getDisplayMedia()`, sending its video as `screen_share` and its sound as `screen_share_audio`. Each side's sources reach the other as they are, so a screen share arrives as `remote.screen` beside the camera.

## Why a call ended

| Reason | |
|---|---|
| `hangup` | a side hung up |
| `cancelled` | the caller hung up before an answer |
| `rejected`, `busy` | the callee declined, or said they were busy |
| `no_answer` | nobody answered within 45 seconds |
| `page_closed` | a side closed its page |
| `peer_lost` | a side's connection didn't come back within 45 seconds |
| `room_lost` | the room it moved to gave up |
| `not_included`, `forbidden`, `bad_token`, `expired` | the server refused to start it: the plan doesn't include calls, or the token wasn't one it takes ([errors](errors.md)) |
| `no_call`, `call_ended` | answered after the caller had given up |
| `signaling_unavailable` | the page's connection was down when it started |

## What WordPlus Cloud sees

- **Your site decides** every call: the server takes only tokens your site signed, for the people and the call it named, with a key derived from its secret.
- **The two browsers' negotiation is sealed** with a key your site derives from its own WordPress salt, which WordPlus Cloud never sees. The servers relay and store it encrypted, so they can't read the call's addresses or codecs.
- **The media** is encrypted end to end by WebRTC on a direct or relayed path. In a room, the media server forwards it between the two.

## Visitors

A visitor calls, and is called, once your plugin gives them an id: a negative number, which can't be a WordPress user's.

```php
add_filter( 'wordplus_realtime_visitor_id', function ( $id ) {
	$guest = my_plugin_guest_id();   // your own, kept in a cookie
	return $guest ? -1 * $guest : $id;
} );
```

## How it works

- **Lines:** each page holds its user's line on the shared connection, with a token the site signs (`POST /wp-json/wordplus/v1/realtime/calls` with `{ line: true }`).
- **Placing:** the caller's page asks the site for its token (`{ call, id, role: 'caller', to, type }`). Its first join makes the call and rings the callee's lines.
- **Answering:** the callee's page asks for its own (`{ call, id, role: 'callee', from, type }`) and joins. The server lets in only the person the caller rang, for the call it rang about.
- **The engine** is the one-to-one call engine Better Messages runs: it picks the best video codec both sides handle in hardware, restarts ICE on a network change, tries TURN twice before the room, and keeps the call through a reconnect of up to 45 seconds.
