# Plano de Implementação - Chamadas de Áudio e Vídeo com Jitsi Meet ## Opção Escolhida: Docker Local (Desenvolvimento) --- ## 📋 Etapas Fora do Código - Configuração Docker ### Etapa 1: Preparar Ambiente Docker **Requisitos:** - Docker Desktop instalado e rodando - Mínimo 4GB RAM disponível - Portas livres: 8000, 8443, 10000-20000/udp **Passos:** 1. **Criar diretório para configuração Docker Jitsi:** ```bash mkdir -p ~/jitsi-docker cd ~/jitsi-docker ``` 2. **Clonar repositório oficial:** ```bash git clone https://github.com/jitsi/docker-jitsi-meet.git cd docker-jitsi-meet ``` 3. **Configurar variáveis de ambiente:** ```bash cp env.example .env ``` 4. **Editar arquivo `.env` com as seguintes configurações:** ```env # Configuração básica para desenvolvimento local CONFIG=~/.jitsi-meet-cfg TZ=America/Recife # Desabilitar Let's Encrypt (não necessário para localhost) ENABLE_LETSENCRYPT=0 # Portas HTTP/HTTPS HTTP_PORT=8000 HTTPS_PORT=8443 # Domínio local PUBLIC_URL=http://localhost:8000 DOMAIN=localhost # Desabilitar autenticação para facilitar testes ENABLE_AUTH=0 ENABLE_GUESTS=1 # Desabilitar transcrissão (não necessário para desenvolvimento) ENABLE_TRANSCRIPTION=0 # Desabilitar gravação no servidor (usaremos gravação local) ENABLE_RECORDING=0 # Configurações de vídeo (ajustar conforme necessidade) ENABLE_PREJOIN_PAGE=0 START_AUDIO_MUTED=0 START_VIDEO_MUTED=0 # Configurações de segurança ENABLE_XMPP_WEBSOCKET=0 ENABLE_P2P=1 # Limites MAX_NUMBER_OF_PARTICIPANTS=10 RESOLUTION_WIDTH=1280 RESOLUTION_HEIGHT=720 ``` 5. **Criar diretórios necessários:** ```bash mkdir -p ~/.jitsi-meet-cfg/{web/letsencrypt,transcripts,prosody/config,prosody/prosody-plugins-custom,jicofo,jvb} ``` 6. **Iniciar containers:** ```bash docker-compose up -d ``` 7. **Verificar status:** ```bash docker-compose ps ``` 8. **Ver logs se necessário:** ```bash docker-compose logs -f ``` 9. **Testar acesso:** - Acessar: http://localhost:8000 - Criar uma sala de teste e verificar se funciona **Troubleshooting:** - Se houver erro de permissão nos diretórios: `sudo chown -R $USER:$USER ~/.jitsi-meet-cfg` - Se portas estiverem em uso, alterar HTTP_PORT e HTTPS_PORT no .env - Para parar: `docker-compose down` - Para reiniciar: `docker-compose restart` --- ## 📦 Etapas no Código - Backend Convex ### Etapa 2: Atualizar Schema **Arquivo:** `packages/backend/convex/schema.ts` **Adicionar nova tabela `chamadas`:** ```typescript chamadas: defineTable({ conversaId: v.id('conversas'), tipo: v.union(v.literal('audio'), v.literal('video')), roomName: v.string(), // Nome único da sala Jitsi criadoPor: v.id('usuarios'), // Anfitrião/criador participantes: v.array(v.id('usuarios')), status: v.union( v.literal('aguardando'), v.literal('em_andamento'), v.literal('finalizada'), v.literal('cancelada') ), iniciadaEm: v.optional(v.number()), finalizadaEm: v.optional(v.number()), duracaoSegundos: v.optional(v.number()), gravando: v.boolean(), gravacaoIniciadaPor: v.optional(v.id('usuarios')), gravacaoIniciadaEm: v.optional(v.number()), gravacaoFinalizadaEm: v.optional(v.number()), configuracoes: v.optional( v.object({ audioHabilitado: v.boolean(), videoHabilitado: v.boolean(), participantesConfig: v.optional( v.array( v.object({ usuarioId: v.id('usuarios'), audioHabilitado: v.boolean(), videoHabilitado: v.boolean(), forcadoPeloAnfitriao: v.optional(v.boolean()) // Se foi forçado pelo anfitrião }) ) ) }) ), criadoEm: v.number() }) .index('by_conversa', ['conversaId', 'status']) .index('by_conversa_ativa', ['conversaId', 'status']) .index('by_criado_por', ['criadoPor']) .index('by_status', ['status']) .index('by_room_name', ['roomName']); ``` ### Etapa 3: Criar Backend de Chamadas **Arquivo:** `packages/backend/convex/chamadas.ts` **Funções a implementar:** #### Mutations: 1. `criarChamada` - Criar nova chamada 2. `iniciarChamada` - Marcar como em andamento 3. `finalizarChamada` - Finalizar e calcular duração 4. `adicionarParticipante` - Adicionar participante 5. `removerParticipante` - Remover participante 6. `toggleAudioVideo` - Anfitrião controla áudio/vídeo de participante 7. `atualizarConfiguracaoParticipante` - Atualizar configuração individual 8. `iniciarGravacao` - Marcar início de gravação 9. `finalizarGravacao` - Marcar fim de gravação #### Queries: 1. `obterChamadaAtiva` - Buscar chamada ativa de uma conversa 2. `listarChamadas` - Listar histórico 3. `verificarAnfitriao` - Verificar se usuário é anfitrião 4. `obterParticipantesChamada` - Listar participantes **Tipos TypeScript (sem usar `any`):** ```typescript import type { Id } from './_generated/dataModel'; import type { QueryCtx, MutationCtx } from './_generated/server'; type ChamadaTipo = 'audio' | 'video'; type ChamadaStatus = 'aguardando' | 'em_andamento' | 'finalizada' | 'cancelada'; interface ParticipanteConfig { usuarioId: Id<'usuarios'>; audioHabilitado: boolean; videoHabilitado: boolean; forcadoPeloAnfitriao?: boolean; } interface ConfiguracoesChamada { audioHabilitado: boolean; videoHabilitado: boolean; participantesConfig?: ParticipanteConfig[]; } ``` --- ## 🎨 Etapas no Código - Frontend Svelte ### Etapa 4: Instalar Dependências **Arquivo:** `apps/web/package.json` ```bash cd apps/web bun add lib-jitsi-meet ``` **Dependências adicionais necessárias:** - `lib-jitsi-meet` - Biblioteca oficial Jitsi - (Possivelmente tipos) `@types/lib-jitsi-meet` se disponível ### Etapa 5: Configurar Variáveis de Ambiente **Arquivo:** `apps/web/.env` ```env # Jitsi Meet Configuration (Docker Local) VITE_JITSI_DOMAIN=localhost:8443 VITE_JITSI_APP_ID=sgse-app VITE_JITSI_ROOM_PREFIX=sgse VITE_JITSI_USE_HTTPS=false ``` ### Etapa 6: Criar Utilitários Jitsi **Arquivo:** `apps/web/src/lib/utils/jitsi.ts` **Funções:** - `gerarRoomName(conversaId: string, tipo: "audio" | "video"): string` - Gerar nome único da sala - `obterConfiguracaoJitsi()` - Retornar configuração do Jitsi baseada em .env - `validarDispositivos()` - Validar disponibilidade de microfone/webcam - `obterDispositivosDisponiveis()` - Listar dispositivos de mídia **Tipos (sem `any`):** ```typescript interface ConfiguracaoJitsi { domain: string; appId: string; roomPrefix: string; useHttps: boolean; } interface DispositivoMedia { deviceId: string; label: string; kind: 'audioinput' | 'audiooutput' | 'videoinput'; } interface DispositivosDisponiveis { microphones: DispositivoMedia[]; speakers: DispositivoMedia[]; cameras: DispositivoMedia[]; } ``` ### Etapa 7: Criar Store de Chamadas **Arquivo:** `apps/web/src/lib/stores/callStore.ts` **Estado gerenciado:** - Chamada ativa (se houver) - Estado de mídia (áudio/vídeo ligado/desligado) - Dispositivos selecionados - Status de gravação - Lista de participantes - Duração da chamada - É anfitrião ou não **Tipos:** ```typescript interface EstadoChamada { chamadaId: Id<'chamadas'> | null; conversaId: Id<'conversas'> | null; tipo: 'audio' | 'video' | null; roomName: string | null; estaConectado: boolean; audioHabilitado: boolean; videoHabilitado: boolean; gravando: boolean; ehAnfitriao: boolean; participantes: Array<{ usuarioId: Id<'usuarios'>; nome: string; avatar?: string; audioHabilitado: boolean; videoHabilitado: boolean; }>; duracaoSegundos: number; dispositivos: { microphoneId: string | null; cameraId: string | null; speakerId: string | null; }; } interface EventosChamada { 'participant-joined': (participant: ParticipanteJitsi) => void; 'participant-left': (participantId: string) => void; 'audio-mute-status-changed': (isMuted: boolean) => void; 'video-mute-status-changed': (isMuted: boolean) => void; 'connection-failed': (error: Error) => void; 'connection-disconnected': () => void; } ``` **Métodos principais:** - `iniciarChamada(conversaId, tipo)` - `finalizarChamada()` - `toggleAudio()` - `toggleVideo()` - `iniciarGravacao()` - `finalizarGravacao()` - `atualizarDispositivos()` ### Etapa 8: Criar Utilitários de Gravação **Arquivo:** `apps/web/src/lib/utils/mediaRecorder.ts` **Funções:** - `iniciarGravacaoAudio(stream: MediaStream): MediaRecorder` - Gravar apenas áudio - `iniciarGravacaoVideo(stream: MediaStream): MediaRecorder` - Gravar áudio + vídeo - `pararGravacao(recorder: MediaRecorder): Promise` - Parar e retornar blob - `salvarGravacao(blob: Blob, nomeArquivo: string): void` - Salvar localmente - `obterDuracaoGravacao(recorder: MediaRecorder): number` - Obter duração **Tipos:** ```typescript interface OpcoesGravacao { audioBitsPerSecond?: number; videoBitsPerSecond?: number; mimeType?: string; } interface ResultadoGravacao { blob: Blob; duracaoSegundos: number; nomeArquivo: string; } ``` ### Etapa 9: Criar Componente CallWindow **Arquivo:** `apps/web/src/lib/components/call/CallWindow.svelte` **Características:** - Janela flutuante redimensionável e arrastável - Integração com lib-jitsi-meet - Container para vídeo dos participantes - Barra de controles - Indicador de gravação - Contador de duração **Props (TypeScript estrito):** ```typescript interface Props { chamadaId: Id<'chamadas'>; conversaId: Id<'conversas'>; tipo: 'audio' | 'video'; roomName: string; ehAnfitriao: boolean; onClose: () => void; } ``` **Estrutura:** - `