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

pubspec.yaml
dependencies:  lunionmeet_flutter: ^1.1.0

Permissions

Caméra et micro doivent être déclarés sur chaque plateforme.

android/app/src/main/AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA" /><uses-permission android:name="android.permission.RECORD_AUDIO" /><uses-permission android:name="android.permission.INTERNET" />
ios/Runner/Info.plist
<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 :

macos/Runner/*.entitlements
<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.

pubspec.yaml
dependencies:  webview_windows: ^0.4.0
windows_call_screen.dart
import '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éfaut true), 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 est true par défaut (ex. recording: falsemasque l'enregistrement selon le plan ; inviteLink: false masque 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, raiseHand et inviteLink sont 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éfaut true), false masque 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.

server.ts
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 Flutter

3. 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.

call_screen.dart
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() et room.toggleCamera() : couper ou activer le micro et la caméra.
  • room.micEnabled et room.cameraEnabled : état courant.
  • room.shareScreen() et room.stopScreenShare(): partage d'écran.
  • room.switchCamera() : basculer entre caméra avant et arrière.
  • room.sendChat("…") et room.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éfaut true) + callback onModerated(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

Passez e2ee: 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.
e2ee.dart
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.