# 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](/pt/docs/event-room/latest/markdown/setup-harness)); 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 [#1-host-abre-a-tela-cria-a-sala-mostra-o-qr] ```ts 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 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") [#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 [#3-guest-escaneia-entra-com-a-credential-da-sessão] ```ts 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](/pt/docs/event-room/latest/markdown/seguranca)). ## 4. Handshake de prontidão [#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](/pt/docs/event-room/latest/markdown/maquina-de-estados) e [versionamento-e-deploy.md](/pt/docs/event-room/latest/markdown/versionamento-e-deploy) para o que acontece quando diverge. ## 5. Guest captura mic / STT e envia evento tipado [#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](/pt/docs/event-room/latest/markdown/eventos)) e envie: ```ts guest.sendEvent('transcription', { text: 'texto transcrito', partial: false }); ``` ## 6. Host recebe, mostra a transcrição [#6-host-recebe-mostra-a-transcrição] ```ts 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](/pt/docs/event-room/latest/markdown/eventos) e [erros.md](/pt/docs/event-room/latest/markdown/erros)). ## 7. Encerramento [#7-encerramento] ```ts 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: ```ts 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`) [#nota-sobre-o-hook-react-event-roomreact] 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.