Installation
Un module ES depuis le CDN, une source vidéo, et un appel.
Quel CDN
Un module ES, servi par environnement. L’origine depuis laquelle vous l’importez décide de l’environnement que vous mesurez : la librairie en déduit le serveur d’analyse, il n’y a donc rien d’autre à configurer.
| Environnement | URL du module |
|---|---|
| Production | https://cdn.saphere.ai/realtime-measure/v1/main.js |
| Test | https://cdn.test.saphere.ai/realtime-measure/v1/main.js |
La page qui marche en le moins de lignes
<!doctype html>
<video id="camera" playsinline muted></video>
<script type="module">
import RealtimeMeasure from "https://cdn.saphere.ai/realtime-measure/v1/main.js"
const video = document.getElementById("camera")
video.srcObject = await navigator.mediaDevices.getUserMedia({ video: true, audio: false })
await video.play()
const measure = RealtimeMeasure.create({
video: "#camera",
retrieveAccessToken: () => fetch("/mon-serveur/realtime-token").then(response => response.text()),
onEvent: event => {
if (event.type === "measure:variables")
console.log(event.variables.heartRate)
}
})
await measure.start()
</script>
Le module se pose aussi comme window.IVirtual.RealtimeMeasure et émet sur window un CustomEvent realtime-measure:ready qui porte la classe dans son detail. C’est la porte d’une page qui ne peut pas écrire d’import : donnez l’URL du CDN comme src d’une balise de script de type module, et écoutez l’événement plutôt que de lire la globale, qui n’est pas garantie posée quand votre propre script en ligne s’exécute. La balise doit porter type="module" — le paquet est un module ES, et une balise ordinaire s’arrête sur SyntaxError: Cannot use 'import.meta' outside a module sans rien exécuter. Le paquet refuse par ailleurs d’être évalué deux fois : plusieurs mesures s’obtiennent par plusieurs create() sur la même classe.
Ce que le navigateur doit fournir
Quatre exigences, dont les deux premières écartent d’emblée certains navigateurs.
- Un contexte sécurisé. La page doit être servie en
https(ou êtrelocalhost) : la caméra comme WebTransport refusent le reste. - WebTransport. Les navigateurs fondés sur Chromium et Firefox le servent. WebKit n’interopère pas avec notre serveur aujourd’hui — c’est-à-dire Safari, et toutes les WebViews d’iOS, qui sont toutes WebKit quelle que soit leur étiquette. Là, les modèles se chargent et la session ne s’ouvre jamais : vous recevez
realtime:attached, puisrealtime:error. S’il vous faut couvrir ces utilisateurs, Saphere Scan mesure par un websocket qu’ils servent, eux. - WebGL2, sur quoi tourne le détecteur de visage. Sans lui,
start()échoue au lieu de se replier. blob:dans votre CSP, sousworker-srcou lescript-srcsur lequel il retombe. Les workers de compression viennent du CDN, et un navigateur refuse un script de worker d’une autre origine si permissifs que soient ses en-têtes CORS — la librairie charge donc chacun par un scriptblob:d’une ligne, de l’origine de votre page. Sans la directive,start()échoue sur ce que Firefox rapporte comme un script en ligne bloqué, ce qui désigne la mauvaise cause.
Éprouvez tôt le navigateur que vous visez
La ligne sur WebTransport est celle qui surprend tard les intégrations. Elle coûte une minute à vérifier : chargez votre page, appelezstart(), et guettez realtime:error.Ce que fait start(), et ce qu’il coûte
start() branche la source déclarée par l’option video, charge le modèle de détection du visage et les workers de compression, obtient un jeton d’accès et ouvre la session. Le premier appel coûte des secondes et plusieurs mégaoctets ; les suivants non — ce qui est chargé le reste, si bien qu’arrêter puis remesurer ne recharge rien. realtime:attached marque le passage de l’un à l’autre, et porte les dimensions de la source.
Seuls les modèles que vos réglages demandent sont téléchargés, et bodyDetection est actif par défaut : c’est du torse que se lit la fréquence respiratoire, dans le mouvement de la cage thoracique. Il en coûte un second modèle au démarrage et une compression de plus par image. Le couper épargne les deux, et la fréquence respiratoire retombe alors sur la modulation que porte l’onde de pouls, qui est la plus fragile des deux lectures. Réactivé plus tard, le modèle se charge à ce moment-là : le torse manque simplement à getDetection() jusqu’à son arrivée.
Le jeton d’accès est demandé après le chargement, à chaque start() : il est à usage unique et la poignée de main le consomme, donc rien n’est frappé tant que la chaîne n’est pas prête à s’en servir.
await measure.start() // branche, charge, ouvre la session
// … plus tard
await measure.stop() // attend que la session soit fermée ; l'instance reste chargée
measure.destroy() // rend le modèle, les workers et la session
Deux façons d’arrêter
stop() attend. Sa promesse ne rend la main qu’une fois la capture arrêtée, les images déjà prises envoyées, les streams fermés et la session fermée — et c’est à ce moment-là que realtime:stopped arrive à votre gestionnaire, pas avant. L’appeler deux fois attend la même fermeture, et un start() lancé entre-temps l’attend au lieu d’ouvrir une session sur une chaîne qui se ferme.
abort() coupe, et rend la main dans le même tour. Ce qui est en file est jeté. Employez-le là où vous n’avez nulle part où attendre : un composant qu’on démonte, un changement de route, un gestionnaire beforeunload. C’est pour cette raison même que destroy() l’emploie.
await measure.stop() // attend : ce qui a été capturé part, puis la session se ferme
measure.abort() // coupe : ce qui est en file est jeté, l'appel rend la main aussitôt
Dans les deux cas la session est enregistrée fermée chez nous, et non comme un lien tombé, ce qui est ce qui les distingue l’un comme l’autre du fait de quitter la page sans rien dire. Un lien saturé ne peut pas faire pendre stop() : passé sa limite d’attente la session est coupée et la promesse rend tout de même la main — un arrêt qui ne finit jamais serait pire qu’un arrêt brutal.
La source vidéo
La source est l’option video, sous l’une des trois formes :
RealtimeMeasure.create({ video: "#camera" }) // un sélecteur CSS
RealtimeMeasure.create({ video: document.querySelector("video") }) // l'élément lui-même
RealtimeMeasure.create({ video: mediaStream }) // un MediaStream nu
Un sélecteur n’est résolu qu’à start() : vous pouvez écrire vos options avant que la page ne porte l’élément, et demander la caméra entre-temps.
Un élément <video> que vous passez est pris tel quel : la librairie ne le déplace pas, ne le coupe pas, ne le retourne pas et ne le masque pas. Si vous passez un MediaStream à la place, un élément vidéo hors du champ est fabriqué pour vous, et destroy() le retire.
Remettre une autre source à setOptions() la rebranche à chaud, mesure en cours comprise : un second realtime:attached annonce alors les dimensions de la nouvelle, ou realtime:error si elle ne désigne rien. Une charge qui ne dit rien de video, en revanche, ne débranche rien — start() branche, destroy() débranche.
Options
Les options sont validées au moment où vous les passez, et une charge qui ne peut pas s’appliquer lève sans rien changer. setOptions() remplace la charge au lieu de s’y fondre : ce que vous omettez revient à son défaut, ce qui est la seule façon de reprendre un réglage.
| Option | Défaut | Ce qu’elle fait |
|---|---|---|
video | aucun | La source à mesurer : un sélecteur CSS, un élément <video> ou un MediaStream. Exigée par start(). |
fps | 30 | Images capturées et envoyées par seconde, bornée à 15–60. Sous 15, la mesure ne veut plus rien dire. |
faceDetection | true | Envoyer le visage. C’est lui qui porte le pouls ; le couper revient à ne rien mesurer. |
bodyDetection | true | Envoyer aussi le torse. C’est de lui que se lit la fréquence respiratoire ; le couper fait retomber celle-ci sur l’onde de pouls. |
retrieveAccessToken | aucun | Une fonction rendant le jeton de session, ou une promesse de jeton. Voir Jetons d’accès. |
onEvent | aucun | Votre gestionnaire. Tout ce que la librairie rapporte passe par lui. Voir Événements. |
Les réglages de capture prennent effet immédiatement, mesure en cours comprise. getOptions() rend ceux qui s’appliquent.
Charger une fois, mesurer plusieurs
stop() laisse le modèle et les workers chargés, si bien qu’un second start() ouvre une session en quelques millisecondes au lieu de retélécharger quoi que ce soit. Créez une instance par page, pas une par mesure.TypeScript
Le module exporte ses types : une intégration écrite en TypeScript obtient l’autocomplétion des options et un switch exhaustif sur les événements — RealtimeMeasureOptions, RealtimeMeasureEvent, RealtimeStats et RealtimeDetection, à côté de REALTIME_MEASURE_EVENTS, qui porte les noms d’événements en constantes pour les intégrations en JavaScript, où rien d’autre ne les contrôle.