Événements

Un canal pour ce qui arrive, deux lectures pour ce qui bouge à la cadence de capture.

Un seul canal

Tout ce que la librairie rapporte passe par options.onEvent, appelé avec un seul objet portant un type. Deux familles : realtime:* dit où en est la librairie, measure:* porte ce que le serveur mesure.

ÉvénementChargeQuand
realtime:attachedwidth, heightstart() a branché la source et chargé ce qu’il lui fallait ; il ne reste que la session à ouvrir. Réémis quand la source change.
realtime:startedLa session est ouverte et la capture tourne.
realtime:stoppedreasonLa mesure s’est arrêtée : requested une fois stop() achevé, aborted après abort() ou destroy(), transport-closed quand le lien est tombé. Un seul par session.
realtime:destroyedLe dernier événement d’une instance : rien ne suit.
realtime:errormessageLe branchement ou le démarrage a échoué.
token:errorreasonLe jeton n’a pas pu être obtenu.
measure:bvpindexBase, bvpUn point d’onde de pouls, à la cadence de capture.
measure:breathingindex, valueUn point d’onde respiratoire, par image de torse mesurée.
measure:variablesvariablesLes grandeurs, une fois par seconde.
measure:presenceface, body, faceBox, bodyBoxUn sujet est entré dans le champ ou en est sorti.
measure:statsstatsLes compteurs de la session, une fois par seconde.
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
        }
    }
})

Ce que les grandeurs portent

measure:variables porte la forme même que le serveur calcule : une fréquence cardiaque, une fréquence respiratoire et ce dont elle a été lue, des indices de variabilité, un score de qualité, et deux drapeaux qu’il vaut la peine de lire avant d’afficher quoi que ce soit.

ChampCe qu’il faut en faire
heartRate, breathingRateLes deux chiffres qu’un utilisateur attend.
breathingSource, breathingQuality, breathingWindowSD’où la fréquence respiratoire a été lue, avec quelle netteté, et sur combien de secondes.
stars, sqiLa qualité du signal, en cinq crans et en indice brut.
maturefalse tant que la fenêtre est trop courte. Dites « mesure en cours », pas un chiffre.
confidentfalse quand la qualité ne passe pas la barre. Présentez la valeur comme provisoire.
sdnn, rmssd, pnn50, sd1, sd2, lfHfLa variabilité, null quand la qualité n’a pas permis de la calculer.
heartRateIntervals, beats, rejectedLa fréquence par les intervalles, et combien de battements ont été retenus et écartés.
elapsed, healthIndexLes secondes mesurées, et l’indice composite quand un âge a été fourni.

Rien n’arrive du tout tant que la fenêtre est trop courte : un silence au début veut dire pas encore, jamais rien à mesurer. Et un champ de variabilité à null veut dire pas de mesure — jamais zéro, qui serait la lecture d’un cœur parfaitement régulier.

La fréquence respiratoire se lit de deux façons, et breathingSource dit laquelle. motion est le mouvement de la cage thoracique, mesuré sur les images de torse — là où la respiration a lieu. bvp est la lente modulation que porte l’onde de pouls, le repli d’une session qui n’envoie pas de torse. Les deux ne valent pas la même chose : un affichage qui montre le chiffre sans lire sa source montre deux choses différentes sous une même étiquette. breathingQuality, entre 0 et 1, dit avec quelle netteté le rythme s’est détaché du signal, et breathingWindowS l’intervalle sur lequel il a été lu — plusieurs sont essayés à chaque publication et celui sur lequel le signal se résout le mieux est retenu, si bien qu’il change en cours de mesure. Les deux valent null quand la source est bvp.

measure:bvp porte indexBase, qui désigne la capture à laquelle le point appartient. Il ne se déduit pas de l’ordre d’arrivée : une image sans visage n’est jamais envoyée, donc la suite saute. Ces trous sont l’image honnête d’une mesure — un visage sorti du champ, un moment de mouvement — et les dessiner comme des trous plutôt que de refermer le trait est ce qui empêche un lecteur de croire le signal continu.

measure:breathing est la même idée sur le torse : un point par image de torse mesurée, index désignant cette image et sautant de la même façon — une capture où aucun torse n’a été trouvé n’envoie rien. Sa value est le déplacement vertical cumulé de la cage thoracique, en pixels du recadrage, et seule sa variation veut dire quelque chose : son origine est là où se trouvait la première image, et elle dérive lentement. Calez un graphe sur le minimum et le maximum de ce qu’il montre, jamais sur zéro, et lisez-y un rythme plutôt qu’un niveau. La fréquence, elle, est dans les grandeurs.

Rien n'est calculé dans le navigateur

Les points des ondes — pouls comme respiration — arrivent tels que le serveur les a calculés. En tirer votre propre fréquence cardiaque ou respiratoire produirait un second chiffre, différent et sans autorité derrière lui.

Lire la détection, plutôt que se la faire annoncer

Ce que le détecteur voit bouge à la cadence de capture. En faire un événement mettrait trente messages par seconde sur un canal qui, en intégration mobile, traverse un pont de WebView — c’est donc une lecture :

function drawOverlay() {
    const { source, face, body } = measure.getDetection()
    if (face !== null) {
        face.box              // { left, top, width, height } en pixels de la source — le recadrage envoyé
        face.normalizedBox    // le même, entre 0 et 1 : à multiplier par votre taille d'affichage
        face.landmarks        // 478 points, { x, y, z }, normalisés
    }
    // … dessinez, puis :
    requestAnimationFrame(drawOverlay)
}

Appelez-la depuis votre propre boucle de rendu. Le cadre vient dans les deux repères parce qu’il fait huit nombres ; les points ne viennent que normalisés, parce qu’il y en a jusqu’à mille quatre cents et que source ({ width, height }) suffit à les replacer en pixels. Le cadre du torse n’est pas la boîte de ses points : il est bâti sur les seules épaules et hanches — un poignet levé l’étirerait sinon sur toute l’image — tandis que landmarks vous rend les trente-trois.

Le détecteur tourne entre start() et le geste qui met fin à la mesure — stop(), abort() ou destroy() : avant une session, face et body valent null.

measure:presence vous dit tout de même quand un sujet entre ou sort, ce qui est la partie qu’il faudrait sinon guetter, et il porte faceBox/bodyBox — le cadre à cet instant, dans les deux repères. Il n’est pas suivi ensuite : c’est à cela que sert la lecture ci-dessus.

Compteurs

getStats() rend des compteurs cumulés — images capturées et sautées, détections, octets envoyés par flux, octets épargnés par la compression, la file d’écriture et son pic, et l’aller-retour d’une image jusqu’à son point d’onde. Le même objet arrive comme measure:stats une fois par seconde, si bien que vous avez rarement à l’appeler vous-même.

Cumulés, jamais une cadence : une cadence demande une fenêtre, et cette fenêtre est une décision d’affichage. Faites la différence de deux relevés si vous voulez un débit.

Deux d’entre eux se lisent mal. savedBytes est la somme de faceSavedBytes et de bodySavedBytes, si bien que compter les trois compte l’épargne deux fois. Et latencySamples dit sur combien d’allers-retours les trois valeurs de latence sont assises : zéro veut dire que rien n’est encore revenu, ce qu’un affichage doit distinguer d’une latence nulle.

Une file qui ne redescend pas

queued et queuedPeak sont le signe avancé que la capture produit plus vite que le lien n’absorbe — visible bien avant que la latence ne le montre. Baisser fps est le levier.