Sofya Developers

Receita: celular vira microfone da sala

Receita: celular vira microfone da sala

Pergunta: "como faço o celular virar microfone da sala?"

Jornada completa, host e guest lado a lado, do jeito que acontece de verdade — nenhum passo pulado.

Pré-requisitos: harness rodando (setup-harness.md); credencial de transporte nas duas pontas; guestUrl apontando para uma página/app real de guest (aqui, o default documentado é a PWA hospedada pela própria plataforma com login Sofya — mas guestUrl é um parâmetro do integrador: aceita qualquer guest que você quiser apontar ali, a PWA da plataforma é só o valor default por ambiente).

1. Host abre a tela, cria a sala, mostra o QR

import { createHost, buildJoinUrl, defineContract, type JoinTarget } from '@sofya-sdk/event-room';
import { generateQrImageDataUrl } from '@sofya-sdk/event-room/qr';

// Mesmo contract nas duas pontas — ver eventos.md.
const contract = defineContract({
  transcription: { text: 'string', partial: 'boolean' },
});

const { host, room } = await createHost({
  url: 'ws://localhost:8080/ws',
  credential: 'dev-host-credential',
  guestUrl: 'https://guest.sofya.example/join', // PWA da plataforma, login Sofya — default por ambiente
  contract,
  room: {
    // approval_and_code é RECOMENDADO para este caso: barreira humana
    // (aprovação) + código não público, juntos — open/code sozinhos não
    // têm barreira contra um terceiro entrando em paralelo antes do guest
    // legítimo (ver seguranca.md).
    accessMode: 'approval_and_code',
  },
});

const joinUrl = buildJoinUrl('https://guest.sofya.example/join', room.joinPayload as unknown as JoinTarget);
const qrDataUrl = await generateQrImageDataUrl(joinUrl);
// renderize qrDataUrl num <img> na tela do host

// Um pedido chega por celular escaneado — decida aprovar ou rejeitar.
room.onJoinRequest((request) => {
  // `request.displayName` é auto-declarado, nunca verificado (ver
  // seguranca.md) — é dado para a tela mostrar, não prova de identidade.
  const aprovado = confirmarNaTelaDoHost(request.displayName); // decisão do app, não desta lib
  if (aprovado) {
    room.approveJoin(request.id);
  } else {
    room.rejectJoin(request.id);
  }
});

2. Login na ponta do celular (obrigatório — não existe "escaneou, entrou")

Antes de escanear, o celular precisa estar logado na PWA da plataforma (login Sofya) — a credencial de transporte que o guest vai usar no passo 3 vem dessa sessão. O QR não autentica ninguém: ele só carrega o alvo da sala (roomId/accessMode/joinCode), nunca uma credencial. Sem login prévio, escanear o QR abre a PWA, não entra na sala.

3. Guest escaneia, entra com a credential da sessão

import { joinRoom, parseJoinTarget } from '@sofya-sdk/event-room';

const target = parseJoinTarget(window.location.href); // a PWA já abriu com o alvo na query string

const guest = await joinRoom({
  url: 'ws://localhost:8080/ws',
  credential: sofyaSessionCredential, // vem do login do passo 2 — nunca vazio
  target,
  displayName: 'Microfone do celular',
  role: 'mic',
  contract, // o mesmo contract do passo 1
});

Com accessMode: 'approval_and_code', o guest com o código certo fica pendente até o host decidir — é o handler de room.onJoinRequest do passo 1 (room.approveJoin/room.rejectJoin) que resolve isso. Rejeitado, o joinRoom do guest rejeita a promise com JoinRejectedError; a tentativa acaba ali, reconectar é uma nova admissão sujeita a nova aprovação (ver seguranca.md).

4. Handshake de prontidão

Assim que o guest é aprovado, os dois lados trocam versão de protocolo e fingerprint do contrato antes de qualquer evento de app trafegar — ver maquina-de-estados.md e versionamento-e-deploy.md para o que acontece quando diverge.

5. Guest captura mic / STT e envia evento tipado

A captura de áudio e a transcrição (STT) são responsabilidade do app, não desta lib — ela só transporta. Declare o formato do evento com defineContract (ver eventos.md) e envie:

guest.sendEvent('transcription', { text: 'texto transcrito', partial: false });

6. Host recebe, mostra a transcrição

room.on('transcription', (payload) => {
  console.log(payload.text, payload.partial); // mostre isso na tela do host
});

Um evento com nome ou payload fora do contract local nunca chega aqui — vira observação não-terminal event_out_of_contract via room.onObservation (ver eventos.md e erros.md).

7. Encerramento

await room.close(); // close_room + leave, nessa ordem — mata a sessão do guest junto

Diga em voz alta o preço disso: host.close() (ou room.close() sozinho) com a sala viva mata o guest que estava transcrevendo — não é um efeito colateral escondido, é a decisão de desenho (o host é dono do ciclo de vida da sala). A mitigação combinada é justamente esta doc: qualquer app que constrói essa tela sabe, antes de escrever o botão de encerrar, que apertá-lo derruba quem está do outro lado.

Reiniciar depois de fechar a sala é sempre startRoom, nunca createHost de novo no meio do ciclo de vida:

const novaSala = await host.startRoom({ accessMode: 'approval_and_code' });

A primeira sala nasce só de createHost; toda sala seguinte, só de startRoom depois de room.close() — nunca o contrário.

host em si também morre — host.close() é fim de vida do objeto host, não só da sala. Depois disso, host.startRoom() lança usage (host_closed); um host novo exige um createHost novo.

Nota sobre o hook React (event-room/react)

Os passos acima usam os verbos puros (createHost/joinRoom) diretamente — é o caminho ensinado. Se a tela é um componente React, useHost/useGuest (@sofya-sdk/event-room/react) fazem exatamente essa mesma chamada por você, uma vez, na primeira renderização. A única coisa contraintuitiva a saber: desmontar o componente não encerra nada — nem room.close(), nem host.close()/guest.close(). Encerrar é sempre um gesto explícito do app (um botão), nunca uma consequência do componente sumir da árvore.

Last updated on

On this page