Skip to main content
Markdown

How calls and live streams work

How a call, a group call and a live stream start, who gets in, how you run them while they last, and how they end. Every use case builds on this page.

Nothing to create or delete​

A call, a group call and a live stream each have a name your app picks, such as support:34:42, team:7 or launch:mug-2. There is nothing to create before and nothing to delete after:

  • It starts when a page uses the name, and your rule for that name lets the person in.
  • It ends when its people leave, or when your server or its host ends it.
  • Your server decides who gets in, with a rule for each kind of name, and runs it while it lasts.
Starts whenWho gets inWho runs itEnds when
A call between two peoplea page calls someoneyour calls() rule, for the caller and again for the one who answersthe two peopleone of them hangs up, or nobody answers within 45 seconds
A group callthe first person joinsyour groupCalls() rule, for each person who joinsyour serveryour server ends it, or the last person leaves
A live streamits host opens it, then goes liveyour streams() rule, which gives each person a roleits host, those who keep its chat, and your serverits host or your server ends it, and it may go live again

How a page asks your server​

A rule is a function your server registers once for a pattern of names, such as support:{a}:{b}. Say Ben, user 42, calls Ann, user 34, as in calls from a button, which builds it step by step. WordPlus Cloud is not asked first. It goes like this:

  1. The SDK in the page of Ben sends a request to your server, to the route you added once when you set the package up (getting started): POST /api/realtime/calls, with the name of the call and whom he calls. It is an address on your own website, /api/realtime unless you choose another, and it carries his usual cookie, as every request from your page does.
  2. The WordPlus package on your server answers it. It finds who is asking the way your app does, with your user() function (your session, or auth() in Laravel), then the rule whose pattern fits support:34:42, and runs it for Ben and Ann.
  3. A yes comes back signed. The package signs it with your secret into a token for this call alone. A no comes back as forbidden, and nothing rings.
  4. The page hands the token to WordPlus Cloud, which checks the signature and rings Ann. Her page asks your server the same way when she answers.
page of Ben      →  your server       POST /api/realtime/calls  { call: 'support:34:42', to: 34 }
your server → page of Ben the rule says yes: a token signed with your secret
page of Ben → WordPlus Cloud the token
WordPlus Cloud → pages of Ann ring

WordPlus Cloud does not ask your server again: it trusts only what your secret signed, so a page can neither skip your rule nor change what it answered. Rooms ask the same way at /api/realtime/auth, and group calls and live streams at /api/realtime/calls.

What a rule answers:

  • false keeps them out, with the error forbidden on the page.
  • true lets them in, with everything that kind allows.
  • An object lets them in with what it says, such as { publish: false } for someone who listens in a group call, or a role in a live stream.
  • It runs again each time their token renews, every 6 hours by default, so a change in your data counts by then. To take someone out at once, see taking access away.

Your server or ours​

Only the last two lines of the picture above reach WordPlus Cloud. The request to /api/realtime/calls goes to your own website, like a request to any other route of yours: WordPlus Cloud never sees it, your rules or your sessions. It sees only the token your server signed.

The requestGoes toAnswered by
POST /api/realtime/calls, /auth and /keys, from your pagesyour serverthe WordPlus package on it, running your rules
the signed token, then the call or the stream itselfWordPlus CloudWordPlus Cloud
a webhook such as call.ended, from WordPlus Cloudyour server, at /api/realtime/webhookthe WordPlus package, then your on() handlers

Your server takes no part in the call itself. Its sound and video never pass through it.

Any address you like​

/api/realtime is only where the routes go unless you say otherwise. Move them all under another start with base, or give each route an address of its own with routes. They stay on the same website as your pages, since they answer no other site.

The page needs no change either way. Your server writes the addresses of the routes into the settings of the page (realtime.config( user )), and the SDK in the page sends its requests where they say.

All of them under another start​

Such as /my/own/address/auth, /my/own/address/calls and so on. Set the start in the two places that must match: where you add the routes, and the base option.

const realtime = createRealtime( {
// Your keys, as before, then the address of the routes.
base: '/my/own/address',
} );

app.use( '/my/own/address', realtime.node( ( req ) => req.session.user ?? null ) );

Each at an address of its own​

Such as /check-call for the calls route. Name each address in routes, and the package answers each route there alone. A route you leave out stays at /api/realtime/{name}, or under your base.

const realtime = createRealtime( {
// Your keys, as before, then each route where you want it.
routes: {
auth: '/permit/rooms',
calls: '/check-call',
keys: '/my/keys',
webhook: '/hooks/wordplus',
},
} );

// Once for the whole app: it answers those four addresses and hands every other request on.
app.use( realtime.node( ( req ) => req.session.user ?? null ) );

// Next.js: a file for each address, such as app/check-call/route.ts
export const POST = realtime.route;

Moved the webhook, with base or in routes? Then also change the webhook address on your app page in your WordPlus Cloud account, such as https://your-site.com/my/own/address/webhook or https://your-site.com/hooks/wordplus. A phone app has no website of its own, so it names the full addresses: endpoint, such as https://your-site.com/my/own/address, and in routes any route at an address of its own. Made from your server config(), it has them all already.

Calls between two people​

Starting one​

The page calls someone, by the name of the call and their user id:

const calls = await rt.calls();
const call = await calls.start( 'support:34:42', { to: 42, type: 'video' } );

Or a ready-made button, with the call screen once on the page: <wordplus-call to="42" call="support:{pair}" type="video">Call support</wordplus-call> and await rt.callScreen().

It rings on every page and app user 42 has open with the call screen or calls.on( 'incoming' ), and stops ringing everywhere once one answers:

calls.on( 'incoming', async ( incoming ) => {
const call = await incoming.accept(); // or incoming.decline( 'busy' )
} );

Who may call​

Your calls() rule runs twice: for the caller before it rings, and for the one who answers (calls from a button).

How it connects​

A call between two people always connects, whatever the network, and changes its path on its own, without hanging up:

PathWhen
1. Directly between the two deviceson most networks. The fastest: the picture and the sound go from one device to the other
2. Through our relayswhen a firewall or a network blocks the direct path. The relays answer on the ports of the web, 443 and 80, so a call gets through wherever websites do
3. Through our media serverswhen even the relays cannot carry it. The call moves into a room on our media servers, and goes on
  • Encrypted end to end on every path, with a key only your server gives out: our relays and media servers carry what they cannot read.
  • No credits on any path. Calls between two people are in every plan, relayed or not.
  • A phone that moves from Wi-Fi to mobile data finds a new path, and the call goes on.

Ending it​

call.end();                            // hang up
call.on( 'state', ( state ) => {} ); // ringing, connecting, connected … ended
call.endReason; // why: hangup, rejected, no_answer …

It also ends on its own when nobody answers within 45 seconds, when a page closes, or when a connection is gone for more than 45 seconds (why a call ended).

From your server: a call between two people runs between the two people, so your server neither lists it nor ends it. To stop someone calling, your rule answers no to their next call, and realtime.user( id ).disconnect() takes them out of the call they are in now.

Group calls​

Starting one​

There is nothing to create: the first person who joins a name opens the call, and everyone else joins the same name.

const meeting = await calls.group( 'team:7', { type: 'video' } );

Or a ready-made button: <wordplus-room room="team:7" type="video" title="Design team">Join the meeting</wordplus-room>.

To bring people in, show the button on their page, or tell them from your server, and their page joins:

await realtime.toUser( [ 43, 56 ] ).emit( 'meeting', { name: 'team:7' } );
rt.on( 'meeting', async ( { name } ) => showMeeting( await calls.group( name, { type: 'video' } ) ) );

Who may join​

Your groupCalls() rule, each time someone joins:

The rule answersThey
falseare refused with forbidden
truejoin, send their camera, microphone and screen, and see and hear everyone
{ publish: false }join as a listener: they see and hear, and send nothing
{ hidden: true }join unseen, listed to nobody

Running it​

On the page, each person sends what they choose and leaves when they want:

await meeting.setTrack( 'camera', cameraTrack );   // null stops it
meeting.leave();

Everything else is for your server, with no page open:

const meeting = realtime.call( 'team:7' );
await meeting.get(); // who is in it, and what each sends
await meeting.permit( 43, { publish: true } ); // let 43 speak, and false makes them a listener again
await meeting.mute( 43, 'microphone' ); // a microphone left on
await meeting.remove( 43 ); // out now
await realtime.listCalls(); // every group call going on

Buttons for the host​

A page cannot mute or remove someone else itself. The buttons of the host call a route of yours, which checks who asks and does it:

app.post( '/meetings/:id/remove', async ( req, res ) => {
if ( !( await isLeadOf( req.params.id, req.user.id ) ) ) return res.sendStatus( 403 ); // only the lead
await realtime.call( 'team:' + req.params.id ).remove( req.body.user );
res.sendStatus( 204 );
} );
removeButton.onclick = () => fetch( '/meetings/7/remove', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify( { user: 43 } ),
} );

Letting someone speak, muting and ending work the same way (classes).

Ending it​

It closes on its own once the last person leaves. Your server ends it for everyone at once:

await realtime.call( 'team:7' ).end();

Everyone is out, and the tokens signed before do not open it again. The same name opens a new call later, for whom your rule lets in.

Live streams​

Starting one​

The host opens the stream by its name, sees their camera, then goes live:

const show = await calls.stream( 'launch:mug-2' );
await show.preview(); // the camera and microphone, for the host alone
await show.goLive(); // everyone watching sees it now

Or ready-made: <wordplus-stream name="launch:mug-2" title="The new mug"></wordplus-stream> is the player for viewers, and for the host the studio, with Go live and End stream. From OBS, const { url, key } = await show.ingest().

Viewers open the same name with the same element, or calls.stream( name ) and show.watch( video ). Before the host goes live, they see it waiting.

Who is who​

Your streams() rule gives each person a role:

The rule answersThey
falseare refused with forbidden
'viewer'watch, write in the chat and raise a hand
{ role: 'viewer', chat: 'read' }watch and read the chat, without writing
{ role: 'viewer', mod: true }watch and keep the chat
'host'go live, bring people on stage, keep the chat and end the stream

user is null for a visitor who is not signed in: 'viewer' lets visitors watch, and they sign in to write or raise a hand, while false keeps them out (members-only streams).

Running it​

The host and those who keep the chat, on the page:

await show.chat.delete( id );
await show.chat.pin( id );
await show.chat.timeout( user, 600 ); // no writing for 10 minutes
await show.kick( user ); // out now, and they may come back
await show.ban( user ); // out, and kept out

Your server, with no page open:

const show = realtime.stream( 'launch:mug-2' );
await show.get(); // live or not, and how many watch
await show.say( 'Free shipping until 8 pm', { name: 'Mug Shop' } );
await show.ban( 88 );
await realtime.listStreams(); // every stream live now

Your server hears each stream go live and end, from the webhook: stream.live and stream.ended (live streams).

Ending it​

The host ends it with End stream, or await show.end(). Your server ends it too:

await realtime.stream( 'launch:mug-2' ).end();

The host may take it live again later under the same name. Its chat stays for a day after its last message, unless chat: { fresh: true } empties it each time it goes live.

Taking access away​

ToYour server
keep someone out from now onchanges your data: your rule answers no when they next join, and at the renewal of their token
take someone out of a group call nowrealtime.call( name ).remove( user )
take someone out of a live stream nowrealtime.stream( name ).kick( user ), or ban( user ) to keep them out
take someone out of a room nowrealtime.in( room ).kick( user ), and your rule saying no for them keeps them out
take a user out of everything nowrealtime.user( id ).disconnect(): every room, call, group call and live stream, on every device, and their pages ask your rules again

More in permissions.