headless-mode

Mode headless : construire sa propre interface sans @legba-core/ui

Ce que ça fait

@legba-core/ui (Lit, Web Components) est une interface possible, pas la seule. Le socle réel — connexion, publication micro/caméra, présence des participants, pistes distantes — vit entièrement dans createLegbaRoom() (@legba-core/realtime/client), qui ne dépend d’aucun moteur de rendu : ni Lit, ni React, ni aucun DOM particulier. Une application qui veut son propre design construit sa propre interface directement sur createLegbaRoom(), sans jamais importer @legba-core/ui.

Exemple complet et vérifié : examples/headless-call — une interface d’appel minimale (DOM/CSS entièrement maison), testée en E2E contre un vrai serveur LiveKit (examples/headless-call/e2e/).

Exemple

import { createLegbaRoom, type LegbaClientRoom } from "@legba-core/realtime/client";

async function joinCall(serverUrl: string, token: string, container: HTMLElement): Promise<LegbaClientRoom> {
  const room = createLegbaRoom();

  room.on("trackSubscribed", ({ participantIdentity, kind, track }) => {
    if (kind !== "video") return;
    const video = document.createElement("video");
    video.autoplay = true;
    video.srcObject = new MediaStream([track]);
    video.dataset.identity = participantIdentity;
    container.appendChild(video);
  });

  room.on("trackUnsubscribed", ({ participantIdentity }) => {
    container.querySelector(`video[data-identity="${participantIdentity}"]`)?.remove();
  });

  await room.connect({ serverUrl, token });
  await room.publishMicrophone();
  await room.publishCamera();
  return room;
}

Rien de plus n’est requis : pas de composant à enregistrer, pas de Shadow DOM, pas de jeton --legba-* à respecter — l’application décide entièrement de sa propre structure DOM et de son propre CSS.

Pièges

  • createLegbaRoom() n’a jamais dépendu de Lit. Ce n’est pas une extraction faite pour ce billet : c’était déjà vrai depuis LGB-013 (packages/realtime/src/legba-room.ts), simplement jamais démontré par un exemple dédié avant LGB-034.
  • getRemoteParticipants() reflète l’état déjà connu à tout instant — utile pour un rendu initial (participants déjà présents avant que participantConnected n’ait la moindre chance de se déclencher pour eux, voir le piège documenté sur cet événement), en plus des événements pour le temps réel.
  • La tuile locale ne doit jamais avoir de sortie audio (se réécouter soi-même produit un larsen immédiat) — getRemoteParticipants() exclut déjà l’identité locale par construction (voir remote-participants.ts), mais une interface headless doit quand même veiller à ne jamais construire elle-même un <audio> à partir de sa propre piste micro locale.
  • .raw reste la même échappatoire typée que pour <legba-call> (voir raw-escape-hatch.md) pour tout ce que LegbaClientRoom n’enveloppe pas encore (ex. lire la piste caméra locale déjà publiée).
  • Aucune fonctionnalité de @legba-core/ui n’est reconstruite automatiquement. Tableau blanc, modération, partage d’écran, arrière-plan virtuel : chacun a sa propre logique dans @legba-core/realtime, mais le rendu (comme <legba-whiteboard>) reste à reconstruire pour une interface headless qui en aurait besoin — rien de spécifique au mode headless lui-même, juste plus de travail d’intégration.