Voice
The map is at The guide. This page is the whole of voice in an app: joining a call, speaking, hearing, and what it costs.
An app needs no key and no server of its own for voice. Its pages talk through
the app's own door, ./api/rtc/, which the platform keeps open onto Cloudflare
Realtime. The WebRTC half is a package the page imports:
import { join, microphone } from 'https://esm.sh/jsr/@yaks/rtc@0.2.5'Speaking
A page joins as an entity: whatever the person is in the app, a player or a seat
at a table. write is how the page writes that entity, and it has to relay the
rtc component to the other pages rather than store it, which @yaks/client's
mutate does:
let call = await join({ entity: me, write: (b) => store.mutate(b) })
talk.onclick = async () => await call.publish(await microphone())The browser asks the person for the microphone, so publish from a click or a key
press. From then on the entity wears
rtc { session, tracks: ['voice'], muted, talking } on every page watching it,
and only while the tab is open: nobody keeps rtc, so it goes when the page
does.
call.mute(true)stops sending without leaving, andrtc.mutedsays so.rtc.talkingis true while the microphone hears a voice, for a page that draws who is speaking.call.leave()ends the call and clearsrtc.join({ change })is told when the call moves:live,lostwhile it reconnects,limitwhen the allowance is spent,refusedwhen this person may not speak here.
Hearing
A page hears the voices it asks for and nobody else's, so it asks only for the ones it wants: the people at the same table, or within earshot.
let voice = await call.hear(them.rtc.session, 'voice')
let out = new Audio()
out.srcObject = voice.stream
await out.play()voice.stop() stops hearing it. A voice whose speaker left, or whose connection
was rebuilt, reads voice.live == false; ask for it again from the speaker's
new rtc.
To place a voice in space, feed the stream into Web Audio instead:
let source = audio.createMediaStreamSource(voice.stream)
source.connect(panner).connect(audio.destination)The stream hear hands back is already playing through a muted <audio>, which
is what Chrome needs before Web Audio hears anything from it.
The browser's echo canceller hears what an <audio> element plays and not what
Web Audio plays: in Chrome, a voice placed in space comes back to its speaker
through a listener's loudspeakers. Headphones avoid it.
Who may speak
Voice costs the space money, so by default only the app's members who may change
it speak and listen. An app that wants its visitors to talk too says so at the
top of its vocab.json:
{ "rtc": "open", "$defs": { … } }Then anyone who may write the app may, which on an open app is anyone with the
link, at up to 60 calls to the door a minute each.
What it costs
A voice is weighed as it is heard: about $0.001 for each voice a page hears for an hour, and nothing for speaking. Ten people each hearing three others talk for an hour is about $0.03.
Voice spends the same allowance as models: $0.20 a month on a free space, shared
by the free spaces its owner has, and $3.00 on the Plus plan. app_list says
how much of it the month has spent, and how much of that was voice. When it is
spent, a new call and a new voice are refused (limit), the voices already
heard go quiet within a minute, and everything else in the app keeps working.
Voice comes back on the 1st, or once the space moves to the Plus plan.
