Intégration sans SDK
Intégrez lunion.meet depuis n'importe quel langage ou plateforme (Java, Go, PHP, Python, .NET…), même sans SDK officiel. Tout passe par HTTP, un JWT HS256 et, côté client, la page d'appel embarquée ou le client-js.
Le principe : votre serveur émet, votre client rejoint
Le WebRTC se déroule côté client. Votre backend ne fait que deux choses : (1) émettre un jeton d'accès pour un utilisateur, (2) le transmettre à votre client, qui rejoint la salle. Optionnel : recevoir des webhooks.Les 3 étapes
- 1. Serveur: créez une salle (une fois) et émettez un jeton par participant. Deux voies : l'API REST, ou signer le JWT vous-même.
- 2. Client : passez
token+urlà la page embarquée/embed/call(iframe web ou WebView native) ou au@lunionlab/meet-client-js. - 3. Webhooks (optionnel) : réagissez aux événements (arrivée, départ, fin de session…) en vérifiant la signature HMAC.
1. Émettre un jeton (côté serveur)
Voie A : API REST (recommandée, aucune crypto)
Authentifiez chaque requête avec votre clé d'API en en-têteAuthorization: Bearer <API_KEY>. C'est l'API qui signe le jeton, votre code ne fait que des appels HTTP, dans n'importe quel langage.
/api/v1/sdk/rooms/api/v1/sdk/rooms/:slug/token# 1) Créer une salle -> renvoie { "slug": "calm-otter-1289", ... }curl -X POST https://meet.lunion-lab.com/api/v1/sdk/rooms \ -H "Authorization: Bearer $LUNION_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Réunion équipe"}' # 2) Émettre un jeton pour un participant de cette sallecurl -X POST https://meet.lunion-lab.com/api/v1/sdk/rooms/calm-otter-1289/token \ -H "Authorization: Bearer $LUNION_API_KEY" \ -H "Content-Type: application/json" \ -d '{"identity":"user-42","name":"Awa","ttlSeconds":3600}'# -> { "token":"eyJhbGci...", "url":"wss://meet.lunion-lab.com/sfu",# "room":"calm-otter-1289", "identity":"user-42", "expiresAt":1782900000 }Transmettez token et url à votre client. Restreignez les droits avec un objetgrants dans le corps (voir plus bas).
Voie B : Signer le JWT vous-même (hors-ligne)
Si vous hébergez votre instance, vous connaissez le secret partagé et pouvez signer le jeton localement (zéro appel réseau). Le jeton est un JWT HS256signé avec LUNION_TOKEN_SECRET (identique au SFU), dont l'émetteur (iss) doit valoir LUNION_API_KEY.
{ "iss": "lunion", // = LUNION_API_KEY (émetteur attendu par le SFU) "sub": "user-42", // identité stable du participant (obligatoire) "iat": 1782893400, "nbf": 1782893400, "exp": 1782915000, // expiration (secondes epoch) "jti": "b1c2...-uuid", "name": "Awa", // optionnel : nom affiché "video": { // droits (grants) "roomJoin": true, // obligatoire "room": "calm-otter-1289", // slug de la salle "canPublish": true, "canSubscribe": true }}Le SFU se connecte ensuite sur url = wss://VOTRE-HOTE/sfu.
// build.gradle : implementation 'io.jsonwebtoken:jjwt-api:0.11.5'// runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5','io.jsonwebtoken:jjwt-jackson:0.11.5'var key = Keys.hmacShaKeyFor(System.getenv("LUNION_TOKEN_SECRET").getBytes(StandardCharsets.UTF_8));Instant now = Instant.now();String jwt = Jwts.builder() .setIssuer(System.getenv("LUNION_API_KEY")) // ex. "lunion" .setSubject("user-42") .setIssuedAt(Date.from(now)) .setNotBefore(Date.from(now)) .setExpiration(Date.from(now.plusSeconds(6 * 3600))) .setId(java.util.UUID.randomUUID().toString()) .claim("name", "Awa") .claim("video", java.util.Map.of( "roomJoin", true, "room", "calm-otter-1289", "canPublish", true, "canSubscribe", true)) .signWith(key, SignatureAlgorithm.HS256) .compact();import jwt, os, time, uuid # pip install pyjwtnow = int(time.time())token = jwt.encode({ "iss": os.environ["LUNION_API_KEY"], "sub": "user-42", "iat": now, "nbf": now, "exp": now + 6 * 3600, "jti": str(uuid.uuid4()), "name": "Awa", "video": {"roomJoin": True, "room": "calm-otter-1289", "canPublish": True, "canSubscribe": True},}, os.environ["LUNION_TOKEN_SECRET"], algorithm="HS256")Voie B = uniquement si vous hébergez
Signer soi-même exige de connaîtreLUNION_TOKEN_SECRET, le secret partagé avec le SFU. Sur une instance que vous n'administrez pas, vous ne l'avez pas : utilisez la Voie A (clé d'API). Ne mettez jamais ce secret ni la clé d'API dans une app cliente.Droits du jeton (grants)
Sous la clé video du JWT (ou l'objet grants de la Voie A) :
roomJoin(obligatoire) +room= slug de la salle.canPublish: publier caméra et micro (défaut vrai).false= spectateur.canSubscribe: recevoir les autres participants (défaut vrai).canPublishData: chat et réactions (défaut vrai).roomAdmin: modération (couper, exclure, rôles).hidden: participant masqué (bot, enregistreur).
2. Connecter le client (sans SDK)
Option 1 : Page d'appel embarquée (la plus simple)
/embed/callest une page d'appel complète et chromeless : chargez-la dans uneiframe (web) ou une WebView (natif), puis poussez-lui la config par postMessage. Aucun SDK client à intégrer.
<iframe id="call" src="https://meet.lunion-lab.com/embed/call" allow="camera; microphone; display-capture; autoplay" style="width:100%;height:100vh;border:0"></iframe><script> const frame = document.getElementById('call'); // La page émet "lunion:ready" quand elle est prête -> on pousse la config. window.addEventListener('message', (e) => { if (e.data?.type === 'lunion:ready') { frame.contentWindow.postMessage({ type: 'lunion:config', url: 'wss://meet.lunion-lab.com/sfu', // access.url room: 'calm-otter-1289', // access.room token: 'eyJhbGci...', // access.token (de votre serveur) name: 'Awa', // options : callStyle, videoProfile, features, branding, theme, labels, lang… }, '*'); } if (e.data?.type === 'lunion:left') { /* l'utilisateur a quitté */ } });</script>Toutes les options de configuration
Styles d'appel, marque blanche, thème, libellés, profil vidéo… la liste complète du messagelunion:config est documentée dans le guide Flutter / WebView(même contrat pour l'iframe web).Option 2 : client-js (tout environnement JavaScript)
@lunionlab/meet-client-js (le LunionMeetClient) est agnostique au framework : Vue, Svelte, Angular, vanilla… Vous gérez vous-même l'UI (éléments <video>).
import { LunionMeetClient } from "@lunionlab/meet-client-js";const client = new LunionMeetClient( { url: "wss://meet.lunion-lab.com/sfu" }, { onTrack: (track, streams) => { /* afficher streams[0] */ } },);await client.connect("calm-otter-1289", "Awa", undefined, "eyJhbGci...");await client.publishCamera(); // + client.publishMic()Option 3 : Protocole SFU brut (avancé)
Pour une plateforme native sans SDK, vous pouvez parler directement au SFU : WebSocket surwss://VOTRE-HOTE/sfu/ws?room=&name=&cid=, puis une négociation WebRTC standard : échange d'offer/answer SDP (message { type, sdp }) sur un RTCPeerConnection, aucune notion de RTP capabilities, transports ou produce-consume. C'est le plus de travail ; préférez l'Option 1 (embed) ou le SDK @lunionlab/meet-client-js, dont le code est la séquence de référence.
Jeton d'accès : via sous-protocole WebSocket (hors URL)
Ne mettez PAS le jeton dans l'URL (fuite via logs proxy, historique, Referer). Passez-le comme sous-protocole WebSocket :new WebSocket(url, ["lunion.token." + jwt, "lunion"]). Le serveur répond avec le sous-protocole lunion(jamais le jeton). Les caractères d'un JWT sont valides comme sous-protocole, aucun encodage n'est requis. Un repli ?token= reste accepté pour compatibilité, mais est déconseillé.3. Connexion au SFU : URL, origine, TURN, durée du jeton
- URL du SFU : passez le champ
urlrenvoyé par le token (access.url, ex.wss://VOTRE-HOTE/sfu/ws) tel quel à tous les SDK (web, React Native, Flutter). Il contient déjà/ws: n'en ajoutez ni n'en retirez. (les versions antérieures à 1.0.1 ajoutaient un second/ws, corrigé.) - Origine (navigateur) : le SFU filtre l'
Origindes connexions sans jeton (anti-CSWSH). Une connexion avec un jeton valide n'y est PAS soumise : votre intégration se connecte depuis n'importe quel domainedès lors qu'elle présente un jeton, rien à déclarer côté dashboard. (Seul le first-party sans jeton doit avoir son origine dansSFU_ALLOWED_ORIGINS.) - TURN / NAT: sur 4G, CGNAT ou pare-feu d'entreprise, fournissez des serveurs
iceServers(STUN + TURN) au client, sinon le média peut ne pas s'établir. - Durée du jeton (TTL) : le jeton est vérifié à la connexion, pas en continu : une session ouverte survit à l'expiration, mais une reconnexion exige un jeton encore valide. Pour les appels longs, émettez un TTL couvrant la durée prévue (+ marge), ou ré-émettez un jeton avant de reconnecter.
const client = new LunionMeetClient( { url: access.url, // tel quel, contient déjà /ws iceServers: [ { urls: "stun:stun.l.google.com:19302" }, { urls: "turns:turn.exemple.com:443", username: "u", credential: "p" }, ], }, { onTrack: (track, streams) => attach(streams[0], track) },);await client.connect(access.room, "Awa", undefined, access.token);4. Webhooks (optionnel)
Chaque livraison porte l'en-tête X-Lunion-Signature: sha256=<hex> =HMAC-SHA256 du corps brutavec le secret de votre endpoint. Vérifiez-le en temps constant avant de traiter l'événement.
Mac mac = Mac.getInstance("HmacSHA256");mac.init(new SecretKeySpec(endpointSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));String expected = "sha256=" + HexFormat.of().formatHex(mac.doFinal(rawBody));boolean ok = MessageDigest.isEqual( expected.getBytes(StandardCharsets.UTF_8), request.getHeader("X-Lunion-Signature").getBytes(StandardCharsets.UTF_8));