Skip to main content
Markdown

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

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 } );

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

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:

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​

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), 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).