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 juntoDiga 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