SDK Flutter
Ajoutez les appels audio et vidéo à votre application Flutter sur iOS, Android, web et desktop (Windows, macOS, Linux). Votre serveur émet un jeton, votre application rejoint la salle.
Le flux
Votre serveur crée la salle et émet le jeton avec@lunionlab/meet-server-sdk (voir « Jetons & connexion client »). Votre app Flutter reçoit url, room et token, puis rejoint avec LunionRoom.1. Installer le SDK
dependencies: lunionmeet_flutter: ^1.1.0Permissions
Caméra et micro doivent être déclarés sur chaque plateforme.
<uses-permission android:name="android.permission.CAMERA" /><uses-permission android:name="android.permission.RECORD_AUDIO" /><uses-permission android:name="android.permission.INTERNET" /><key>NSCameraUsageDescription</key><string>Pour les appels vidéo</string><key>NSMicrophoneUsageDescription</key><string>Pour les appels audio</string>Desktop (Windows, macOS, Linux)
Le SDK fonctionne sur desktop. Sur Windows et Linux, aucune permission à déclarer. Sur macOS, ajoutez les entitlements caméra et micro :
<key>com.apple.security.device.camera</key><true/><key>com.apple.security.device.audio-input</key><true/>Windows : alternative WebView
Sur Windows, vous pouvez aussi intégrer l'appel via une WebView, en chargeant la page/embed/call de votre déploiement, sans écrire de code natif. La marche à suivre est décrite dans la section suivante.Windows : repli via WebView (optionnel)
Chargez la page d'appel embarquée /embed/call de votre déploiement dans une WebView (webview_windows, moteur WebView2/Chromium, WebRTC complet) et poussez-lui la config par message. La page émet lunion:ready quand elle est prête ; répondez avec lunion:config. Le token transite par postMessage, jamais dans l'URL.
dependencies: webview_windows: ^0.4.0import 'dart:convert';import 'package:flutter/material.dart';import 'package:webview_windows/webview_windows.dart'; class WindowsCallScreen extends StatefulWidget { const WindowsCallScreen({ super.key, required this.url, // access.url (wss://meet.lunion-lab.com/sfu) required this.room, // access.room required this.token, // access.token (emis par votre serveur) this.name = 'Invite', this.audioOnly = false, // true = UI appel audio (avatars, pas de grille video) this.prejoin = true, // false = rejoindre direct, sans salle d'attente this.lang = 'fr', // 'fr' | 'en' }); final String url, room, token, name, lang; final bool audioOnly, prejoin; @override State<WindowsCallScreen> createState() => _WindowsCallScreenState();} class _WindowsCallScreenState extends State<WindowsCallScreen> { final _web = WebviewController(); @override void initState() { super.initState(); _init(); } Future<void> _init() async { await _web.initialize(); // Autorise camera + micro, sinon getUserMedia cote page echoue ("Permission denied"). _web.permissionRequested = (url, kind, isUserInitiated) async { if (kind == WebviewPermissionKind.camera || kind == WebviewPermissionKind.microphone) { return WebviewPermissionDecision.allow; } return WebviewPermissionDecision.deny; }; _web.webMessage.listen((raw) { final msg = raw is String ? jsonDecode(raw) : raw; switch (msg['type']) { case 'lunion:ready': // la page est prete -> on pousse la config _web.postWebMessage(jsonEncode({ 'type': 'lunion:config', 'url': widget.url, 'room': widget.room, 'token': widget.token, 'name': widget.name, 'audioOnly': widget.audioOnly, 'prejoin': widget.prejoin, 'lang': widget.lang, // Options avancees (voir tableau ci-dessous) : // 'features': {'recording': false, 'chat': true}, // 'branding': {'show': false, 'name': 'Acme Support'}, // 'theme': {'accent': '#7c3aed'}, })); break; case 'lunion:left': // l'utilisateur a quitte l'appel if (mounted) Navigator.of(context).pop(); break; } }); await _web.loadUrl('https://meet.lunion-lab.com/embed/call'); if (mounted) setState(() {}); } @override void dispose() { _web.dispose(); super.dispose(); } @override Widget build(BuildContext context) => Scaffold( body: _web.value.isInitialized ? Webview(_web) : const Center(child: CircularProgressIndicator()), );}Le jeton reste émis côté serveur
Comme pour le natif : votre backend crée salle + jeton avec@lunionlab/meet-server-sdk, puis vous passez url/room/token à WindowsCallScreen. WebView2 est présent d'origine sur Windows 10/11 récents (sinon, embarquez le bootstrapper Evergreen).Caméra/micro : géré par permissionRequested
C'est le point qui bloque le plus souvent : sans autorisation,getUserMediaéchoue côté page (« Permission denied ») et l'appel ne démarre jamais. Le callback permissionRequested du code ci-dessus règle ça (il accorde camera et microphone). Rien d'autre à faire côté Flutter.Configurer l'appel (message lunion:config)
Tout le comportement de l'interface se pilote par le message lunion:config (même page pour la WebView Windows et pour une iframe web). En dehors de url, room, token et name, tous les champs sont optionnels.
callStyle('audio'|'video'|'visio', défaut'visio') choisit le style d'appel :audio= écran téléphone (avatars, sans caméra, caméra jamais allumée) ;video= appel 1:1 immersif (interlocuteur plein cadre + votre caméra en incrustation, les autres en bandeau) ;visio= mosaïque de conférence avec tous les contrôles.audioOnly(booléen) est un raccourci rétro-compatible :trueéquivaut àcallStyle: 'audio'.videoProfile('hd'|'sd'|'ld') règle la charge CPU de la vidéo.hd= 720p30 + 3 couches ;sd= 540p24 + 1 couche ;ld= 360p20 + 1 couche (le mono-couche permet le décodage H264 matériel et un encodage léger). Par défaut, le profil est choisi automatiquement selon la puissance de la machine (seuils plus prudents en WebView) ; ne le fixez que pour forcer une valeur.prejoin(booléen, défauttrue),falserejoint la salle directement, sans salle d'attente (réglage caméra/micro).lang('fr'|'en', défaut'fr'), langue de toute l'interface.features(objet) active/masque chaque fonctionnalité :screenShare,chat,reactions,raiseHand,deviceSelector,pin,recording,inviteLink. En visio tout esttruepar défaut (ex.recording: falsemasque l'enregistrement selon le plan ;inviteLink: falsemasque le code du salon et le bouton « copier le lien » partout : en-tête, barre du bas et menu « Plus »). En appel audio l'interface est épurée :reactions,chat,raiseHandetinviteLinksont masqués par défaut ; réactivez-les au besoin (ex.features: { chat: true }).branding(marque blanche),{ show: false }retire la marque en haut ;{ name: 'Acme' }affiche votre libellé ;{ logo: 'https://…/logo.svg' }affiche votre image (prioritaire sur le texte).header(booléen, défauttrue),falsemasque toute la barre du haut (rendu sans chrome).theme(palette) mappe chaque clé sur une variable CSS :accent,accentForeground,background,foreground,stage(fond),panel(surfaces),line(bordures). Accepte n'importe quelle couleur CSS (hex, rgb, oklch). Ex.{ accent: '#e11d48', stage: '#0b1020' }.labels(objet) surcharge les libellés visibles au-delà de fr/en :leave,mute/unmute,camOn/camOff,share/stopShare,reactions,raiseHand/lowerHand,participants,messages,encrypted,waiting. Ex.{ leave: 'Raccrocher' }.
Trois interfaces distinctes : audio, vidéo, visio
callStyle: 'audio'= écran d'appel téléphonique (avatars centrés, caméra jamais allumée, barre minimale micro/quitter). 'video' = appel 1:1 immersif façon WhatsApp (interlocuteur plein cadre, votre caméra en incrustation, les autres en bandeau). 'visio'(défaut) = mosaïque de conférence avec tous les contrôles. Chaque style a son propre chrome, ce n'est pas la même vue avec des options masquées.Repli dev / iframe : paramètres d'URL
Pour un test rapide en iframe web, la page accepte aussi des paramètres d'URL :/embed/call?url=...&room=...&token=...&name=Awa&lang=en&audioOnly=1&prejoin=0&brand=0&brandLabel=Acme.Sécurité : en production, le token passé dans l'URL est ignoré (fuite via logs/historique/Referer) : poussez-le par postMessage ({ type: 'lunion:config', token }). Les autres paramètres (url, room…) restent lus pour le confort de test.2. Récupérer un jeton (côté serveur)
Comme pour le web, le jeton est émis par votre backend. Ne mettez jamais votre clé d'API dans l'app mobile.
import { RoomServiceClient } from "@lunionlab/meet-server-sdk"; const rooms = new RoomServiceClient( "https://meet.lunion-lab.com/api/v1", process.env.LUNION_API_KEY!,); const room = await rooms.createRoom("Réunion produit");const access = await rooms.createToken(room.slug, "user-42", { name: "Awa" });// → renvoyez access.url, access.room, access.token à votre app Flutter3. Rejoindre la salle
LunionRoom est un ChangeNotifier: écoutez-le pour rafraîchir l'interface. Il gère la connexion, la caméra/micro et l'abonnement aux participants.
import 'package:flutter/material.dart';import 'package:lunionmeet_flutter/lunionmeet_flutter.dart'; class CallScreen extends StatefulWidget { const CallScreen({super.key, required this.url, required this.room, required this.token}); final String url, room, token; @override State<CallScreen> createState() => _CallScreenState();} class _CallScreenState extends State<CallScreen> { late final LunionRoom _room = LunionRoom( sfuUrl: widget.url, room: widget.room, name: 'Awa', token: widget.token, ); @override void initState() { super.initState(); _room.connect(); } @override void dispose() { _room.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: _room, builder: (context, _) => GridView.count( crossAxisCount: 2, children: [ LunionVideoView(stream: _room.localStream, mirror: true), for (final p in _room.participants) LunionVideoView(stream: p.stream), ], ), ); }}Contrôles
room.toggleMic()etroom.toggleCamera(): couper ou activer le micro et la caméra.room.micEnabledetroom.cameraEnabled: état courant.room.shareScreen()etroom.stopScreenShare(): partage d'écran.room.switchCamera(): basculer entre caméra avant et arrière.room.sendChat("…")etroom.onChatMessage: envoyer et recevoir les messages.room.leave(): quitter l'appel et libérer la caméra et le micro.
Fonctionnalités avancées
Réglées à la construction de LunionRoom ou via ses callbacks. Toutes optionnelles et rétro-compatibles.
- Reconnexion réseau automatique (paramètre
reconnect, activée par défaut) : bascule 4G↔wifi, veille, perte de porteuse ou échec ICE. L'état passe àRoomStatus.reconnecting, la connexion est reconstruite et le flux re-publié. - Réception de la modération: coupure de l'hôte appliquée localement (
autoApplyModeration, défauttrue) + callbackonModerated(op). - Mode récepteur pur (
receiveOnly) : rejoint sans publier (écran de projection, spectateur). - Locuteur actif (
activeSpeaker,onActiveSpeaker), enregistrement (startRecording/stopRecording,onRecordingStatus), qualité par couche (setVideoLayerPolicy).
Chiffrement de bout en bout (E2EE) — natif
Passeze2ee: LunionE2ee.fromBase64url('…') à LunionRoom : chaque frame émise/reçue est (dé)chiffrée en AES-GCM par le FrameCryptor natif de flutter_webrtc. Le SFU ne relaie que de l'opaque, la clé ne transite jamais par le serveur — partagez-la hors-bande, la même pour tous les pairs.final room = LunionRoom( sfuUrl: url, room: room, name: 'Awa', token: token, e2ee: LunionE2ee.fromBase64url(sharedKeyB64), // clé 256 bits, hors-bande reconnect: const LunionReconnectConfig(), // activée par défaut);Même protocole que le web
lunionmeet_flutter parle exactement le même protocole que @lunionlab/meet-client-js et @lunionlab/meet-react. Web et mobile peuvent se rejoindre dans la même salle.