Events

One channel for what happens, two reads for what moves at capture rate.

One channel

Everything the library reports goes through options.onEvent, called with a single object carrying a type. Two families: realtime:* says where the library is, measure:* carries what the server measures.

EventPayloadWhen
realtime:attachedwidth, heightstart() has attached the source and loaded what it needed; only the session is left to open. Re-emitted when the source changes.
realtime:startedThe session is open and capture is running.
realtime:stoppedreasonThe measurement stopped: requested once stop() has finished, aborted after abort() or destroy(), transport-closed when the link dropped. One per session.
realtime:destroyedThe last event of an instance: nothing follows.
realtime:errormessageAttaching or starting failed.
token:errorreasonThe token could not be obtained.
measure:bvpindexBase, bvpOne pulse-waveform point, at capture rate.
measure:breathingindex, valueOne breathing-waveform point, per measured torso frame.
measure:variablesvariablesThe vitals, once a second.
measure:presenceface, body, faceBox, bodyBoxA subject entered or left the frame.
measure:statsstatsSession counters, once a second.
RealtimeMeasure.create({
    onEvent: event => {
        switch (event.type) {
            case "measure:bvp":       chart.push(event.indexBase, event.bvp); break
            case "measure:breathing": breathing.push(event.index, event.value); break
            case "measure:variables": panel.show(event.variables); break
            case "measure:presence":  hint.toggle(event.face); break
            case "realtime:error":    banner.show(event.message); break
        }
    }
})

What the vitals carry

measure:variables carries the same shape the server computes: a heart rate, a breathing rate and where it was read from, variability indices, a quality score, and two flags worth reading before you display anything.

FieldWhat to do with it
heartRate, breathingRateThe two numbers a user expects.
breathingSource, breathingQuality, breathingWindowSWhere the breathing rate was read, how clearly it stood out, and over how many seconds.
stars, sqiSignal quality, as five steps and as a raw index.
maturefalse while the window is still too short. Say “measuring”, not a number.
confidentfalse when quality does not clear the bar. Show it as provisional.
sdnn, rmssd, pnn50, sd1, sd2, lfHfVariability, null when quality did not allow computing it.
heartRateIntervals, beats, rejectedThe interval-based rate, and how many beats it kept and dropped.
elapsed, healthIndexSeconds measured, and the composite index when an age was provided.

Nothing arrives at all until the window is long enough, so silence early on means not yet, never nothing to measure. And a variability field at null means no measurement — never zero, which would be the reading of a perfectly regular heart.

The breathing rate is read one of two ways, and breathingSource says which. motion is the rise and fall of the chest, measured on the torso frames — where breathing actually happens. bvp is the slow modulation the pulse wave carries, the fallback for a session that sends no torso. The two are not worth the same, so a display that shows the number without reading its source shows two different things under one label. breathingQuality, between 0 and 1, says how clearly the rhythm stood out of the signal, and breathingWindowS the interval it was read over — several are tried at each publication and the one the signal resolves best on is kept, so it changes during a measurement. Both are null when the source is bvp.

measure:bvp carries indexBase, which identifies the capture it belongs to. It cannot be inferred from arrival order: a frame with no face is never sent, so the sequence has gaps. Those gaps are the honest picture of a measurement — a face that left the frame, a moment of movement — and drawing them as gaps rather than closing the line is what keeps a reader from believing the signal was continuous.

measure:breathing is the same idea on the torso: one point per measured torso frame, index naming that frame and skipping the same way — a capture in which no torso was found sends nothing. Its value is the cumulative vertical displacement of the chest, in pixels of the crop, and only its variation means anything: the origin is wherever the first frame happened to sit, and it drifts slowly. Scale a chart to the minimum and maximum of what it shows, never to zero, and read a rhythm in it rather than a level. The rate itself is in the vitals.

Nothing is computed in the browser

The waveform points — pulse and breathing alike — arrive as the server computed them. Deriving your own heart rate or breathing rate from them would produce a second number, different and with no authority behind it.

Reading the detection, rather than being told about it

What the detector sees moves at capture rate. Turning that into an event would put thirty messages a second on a channel that, in a mobile integration, crosses a WebView bridge — so it is a read instead:

function drawOverlay() {
    const { source, face, body } = measure.getDetection()
    if (face !== null) {
        face.box              // { left, top, width, height } in source pixels — the crop actually sent
        face.normalizedBox    // the same, between 0 and 1: multiply by your display size
        face.landmarks        // 478 points, { x, y, z }, normalized
    }
    // … draw, then:
    requestAnimationFrame(drawOverlay)
}

Call it from your own render loop. The box comes in both frames of reference because it is eight numbers; the points come normalized only, because there are up to fourteen hundred of them and source ({ width, height }) is all you need to place them in pixels. The torso box is not the bounding box of its points: it is built on the shoulders and hips alone — a raised wrist would otherwise stretch it across the whole image — while landmarks gives you all thirty-three.

The detector runs between start() and whichever gesture ends the measurement — stop(), abort() or destroy(): before a session, face and body are null.

measure:presence still tells you when a subject enters or leaves, which is the part you would otherwise have to poll for, and it carries faceBox/bodyBox — the box at that instant, in both frames of reference. It is not tracked afterwards: that is what the read above is for.

Counters

getStats() returns cumulative counters — frames captured and dropped, detections, bytes sent per stream, bytes the compression saved, the write queue and its peak, and the round-trip latency of a frame to its waveform point. The same object arrives as measure:stats once a second, so you rarely need to call it yourself.

Cumulative, never a rate: a rate needs a window, and that window is a display decision. Subtract two snapshots if you want throughput.

Two of them are easy to misread. savedBytes is the sum of faceSavedBytes and bodySavedBytes, so counting all three counts the saving twice. And latencySamples says how many round trips the three latency figures rest on: zero means nothing has come back yet, which a display has to tell apart from a latency of zero.

A queue that does not come back down

queued and queuedPeak are the early sign that capture is producing faster than the link absorbs — visible well before latency shows it. Lowering fps is the lever.