Guide

Real time: UDP, channels, media

Some data cannot wait for a page to ask: a sensor's reading, a player's move, a cursor, a camera's picture. A Quillo program takes UDP packets on a port, passes messages between the server and its pages as they happen, plays a camera's stream in any page, and lets pages send their own camera and microphone to each other. There is no message broker, no socket server beside the app, and no media server: it is the same program, with the same functions and the same checks.

Try it on your machine. These programs need ports, a camera or a sender of their own, so each part below has a command to run. Open the pages at http://127.0.0.1:8080 rather than localhost: see Which way a page connects.

Packets in

udp.listen takes the packets sent to a port, and hands each to a function:

let readings = []udp.listen(9000, (p) => {    let r = json.parse(p.text ?? "")    if is_err(r) { return udp.reply(p, "not JSON") }    readings = readings + [r]    udp.reply(p, "ok")})route GET /readings = readings
quillo run app.qweecho '{"id": "boiler", "v": 71.2}' | nc -u -w1 127.0.0.1 9000     # okecho 'hello' | nc -u -w1 127.0.0.1 9000                           # not JSONcurl 127.0.0.1:8080/readings                                      # [{"id":"boiler","v":71.2}]

Packets out

fn log_line(text: str) with net = udp.send("10.0.0.5", 514, `<14>${text}`)udp.send("10.0.0.7", 9000, { id: "door", open: true })   // a map, as JSONudp.send("10.0.0.7", 9000, [104, 105])                     // numbers, as bytes

Channels: the server and its pages

A channel carries messages between the server and its pages as they happen. A page opens it in a component; the program says, at the top, what to do with what pages send.

channel.on("chat", (msg, peer) => {    let line = { from: peer.session.name, text: msg.text }    channel.broadcast("chat", line, { reliable: true })}, { join: (peer) => peer.session.name != null })component Chat() {    state lines = []    state draft = ""    let chat = channel.open("chat", (m) => { lines = lines + [m] })    <form onsubmit={() => {        chat.send({ text: draft }, { reliable: true })        draft = ""    }}>        <ul>{for l in lines => <li><b>{l.from}</b> {l.text}</li>}</ul>        <input value={draft}/>    </form>}

The page that holds it, and the form that gives a visitor the name join asks for (a page without one is turned away):

route GET / =    <html><body>        {session().name == null            ? <form method="post" action="/name"><input name="name" placeholder="Your name"/><button>Join</button></form>            : <Chat/>}    </body></html>route POST /name { form: { name: str } } { set_session({ name }); redirect("/") }

Open it in two browsers (or one and a private window), each with a name: a line sent in one shows in both, after its sender's name.

Who may join

Anyone who can open a page can ask to join any channel, so a channel whose messages are not for everyone needs join. It gets the peer and says yes or no; above, only pages whose session has a name are let in. A page turned away says so in its console:

quillo: the server did not let this page join channel chat

A channel that no channel.on names can be joined by anyone, to hear what the program broadcasts on it; what pages send on it goes nowhere. leave(peer) hears that a page left the channel, or closed. A page asks to join a room once at a time, and each join that said yes gets exactly one leave, also when the page left or closed while its join was still running, so a count kept by the two (online += 1, online -= 1) stays right.

A room for each tenant

A program with many tenants has a room of each channel for each tenant (since 0.7: one room for all): pages on acme.example.com and evil.example.com joining lobby are in two rooms. What code run for a page or a request broadcasts, sends or lists is its tenant's room's, and a page with no tenant (the bare domain) is in the room of no tenant. A job, a tick and the top level reach every tenant's room, and with_tenant(name, f) one tenant's. channel.send reaches only a page in a room the code reaches. One room for every tenant is asked for:

channel.on("news", (msg) => msg, { tenants: "shared" })

Lost, late, or in order

Not every message has to arrive. A cursor's position or a sensor's reading is out of date by the time the next one comes; a chat line or a "you won" must arrive, and in order. Channels let each message say which it is:

channel.on("cursors", (msg, peer) => {    let at = { id: peer.id, x: msg.x, y: msg.y }    channel.broadcast("cursors", at, { except: peer })         // a datagram}, { leave: (peer) => {    let gone = { id: peer.id, gone: true }    channel.broadcast("cursors", gone, { reliable: true })     // on a stream} })component Cursors() {    state others = {}    let room = channel.open("cursors", (m) => {        others = m.gone ? remove(others, m.id) : set(others, m.id, m)    })    <div.board onpointermove={(e) => room.send({ x: e.clientX, y: e.clientY })}>        {for c in values(others) =>            <i.cursor key={c.id} style={`left:${c.x}px;top:${c.y}px`}/>}    </div>}
MessageOver WebTransportOver a WebSocket
Small (about 1,100 bytes or less, as JSON)A datagram: as soon as it can, but it may come out of order or not at allIn order, none lost while the page keeps up
Larger, or sent with { reliable: true }On a stream: all of them, in orderIn order, none lost while the page keeps up

Over a WebSocket, a page that reads as fast as messages come gets every one, in order, even from a burst of thousands. One that falls behind meets the queue's rules (see the limits): past 4,096 messages waiting for it, a plain one is dropped, and a reliable one closes the connection, so the page joins again.

The positions go as datagrams. A lost one is replaced by the next, a moment later, and nothing waits for it, which is what keeps cursors smooth on a poor connection. "Gone" is sent reliably, so no page keeps a cursor for someone who left. Either side can ask for reliable: room.send(v, { reliable: true }) in a page, channel.broadcast(name, v, { reliable: true }) on the server.

From a device to every page

UDP and channels together pass a device's packets on to pages as they come. No channel.on is needed when pages only listen:

udp.listen(9000, (p) => {    let r = json.parse(p.text ?? "")    if !is_err(r) { channel.broadcast("readings", r) }})component Gauge() {    state last = null    channel.open("readings", (r) => { last = r })    <p>{last == null ? "Waiting for a reading" : `${last.id}: ${last.v}`}</p>}

The Telemetry example goes further: four pretend sensors send readings twice a second, the server keeps the last minute of each, and a page starts from that (a server fn sets its first state) and follows the channel from there.

quillo run telemetry.qwe --watch     # in quillolang/examples; then open http://127.0.0.1:8080quillo run sensors.qwe               # in another terminal

A camera in the page

media.rtp takes a camera's or an encoder's RTP stream, and <quillo-media> plays it in any page, a fraction of a second behind:

media.rtp("cam", { video: 5004, audio: 5006 })media.on("cam", { watch: (peer) => true })     // any page may watch "cam"route GET / = <html><body><quillo-media stream="cam" controls muted/></body></html>

Send it a test picture and a tone with ffmpeg:

ffmpeg -re -f lavfi -i testsrc=size=1280x720:rate=30 -f lavfi -i sine=frequency=440 \  -map 0:v -c:v libx264 -tune zerolatency -profile:v baseline -pix_fmt yuv420p -g 60 \  -f rtp rtp://127.0.0.1:5004 -map 1:a -c:a libopus -f rtp rtp://127.0.0.1:5006

On a laptop, the picture shows about 0.3 seconds after the page opens, at the stream's 30 frames a second.

server fn live_streams() -> list = media.streams()component Stats() {    state streams = live_streams()    every(1000, () => { streams = live_streams() ?? streams })    <p>{for s in streams =>        <span key={s.name}>{s.video.fps} fps · {s.viewers} watching</span>}</p>}

The Camera example is a whole page, with the command for a Mac's own camera and microphone.

Pages that send: a video call

A page can send too. media.on lets pages send a stream (or every stream a name ending in * starts), and <quillo-media publish> sends the page's camera and microphone to it, for every <quillo-media stream> of that name to play. A video call is a stream per person:

media.on("call:*", { publish: (peer) => session().user != null, watch: (peer) => session().user != null })component Call(room: str, me: str, others: list) {    <div>        <quillo-media publish={`call:${room}:${me}`}/>        {for p in others => <quillo-media key={p} stream={`call:${room}:${p}`}/>}    </div>}

Ending a stream

Streams can be made while the program runs: media.rtp in a route or a channel's handler makes one for each room, once for a name. media.close(name) ends one: it stops taking RTP, lets its ports go for another stream to take, and tells its pages, whose data-state turns ended. It gives true if there was such a stream. Code run for a page or a request ends its tenant's stream; a job, a tick or the top level, every tenant's of that name. A room made while the program runs needs a watch too, for its name or a name ending in *: media.on("room:*", { watch: (peer) => peer.session.user != null }).

Which way a page connects

A page opens one connection for all its channels and streams, and opens it again (and joins its channels again) when it drops:

In the Network panel, the connection is the request to /_quillo/wt; if its type is websocket, the page fell back.

On your own machine, open http://127.0.0.1:8080 rather than localhost. quillo run listens on 127.0.0.1, and browsers try localhost over IPv6 first for UDP. They find nothing there and use a WebSocket instead. quillo run main.qwe --host ::1 serves localhost over IPv6, if you would rather keep the name.

In production

Limits, and what it does not do

Which one to use

You wantUse
Pages to show data as it changes in the database or in a variablelive(load)
One answer sent as it is made: a reply word by word, a job's progress[stream, events](/docs/routes/#streaming) with listen or receive
Messages both ways, many a second, between the server and its pagesChannels
Packets from devices or other programsudp.listen
A camera or an encoder, livemedia.rtp and <quillo-media>
Pages' own cameras, microphones or screens, to each other: a video callmedia.on and <quillo-media publish>

The reference for each is under Routes: UDP, Channels and Media.

Thread
15Useful? Like it, or add to its thread below.

Thread 0 posts

On Real time: UDP, channels, media: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.