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).
- 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):
- Node
- PHP
- Python
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 } );
$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 ] );
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
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.loghands you the lines the screen writes, for your own log.
Calls of your own
The same calls, for screens you draw:
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).
call.pathsays what carries it:.direct,.relay(TURN) or.room.call.peerandcall.peerInfowho 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:
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
IncomingCallwhosegroupis set, andaccept()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
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.encryptedsays 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 saysencrypt.- In this version a group call video is drawn by the call screen. Drawing it in screens of your own comes later. Audio,
peopleand 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), 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).