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).
- 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 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.activesays a call is on), andCallScreen.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.loghands you the lines the screen writes, for your own log.
Calls of your own
The same calls, for screens you draw:
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_AUDIOand, for video,CAMERAbefore a call, as for any screen of your own. The library manifest declares them. One allowed only after the call began: callcall.permissionsGranted(), and the call opens it then. call.pathsays what carries it:DIRECT,RELAY(TURN) orROOM.call.peerandcall.peerInfowho the other side is.calls.stopRinging()lets go of this app line: its user calls no longer ring here.start,group,acceptandaddswitch 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:
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
IncomingCallwhosegroupis set, andaccept()answersToGroup. 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
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.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 CallException with the code: forbidden, no_user, not_included, auth_failed (errors).