diff --git a/CORRECOES_JITSI.md b/CORRECOES_JITSI.md new file mode 100644 index 0000000..28a39a1 --- /dev/null +++ b/CORRECOES_JITSI.md @@ -0,0 +1,296 @@ +# Correções Implementadas para Integração Jitsi + +## Resumo das Alterações + +Este documento descreve todas as correções implementadas para integrar o servidor Jitsi ao projeto SGSE e fazer as chamadas de áudio e vídeo funcionarem corretamente. + +--- + +## 1. Configuração do JitsiConnection + +### Problema Identificado +- A configuração do `serviceUrl` e `muc` estava incorreta para Docker Jitsi local +- O domínio incluía a porta, causando problemas na conexão + +### Correção Implementada +```typescript +// Separar host e porta corretamente +const { host, porta } = obterHostEPorta(config.domain); +const protocol = config.useHttps ? 'https' : 'http'; + +const options = { + hosts: { + domain: host, // Apenas o host (sem porta) + muc: `conference.${host}` // MUC no mesmo domínio + }, + serviceUrl: `${protocol}://${host}:${porta}/http-bind`, // BOSH com porta + bosh: `${protocol}://${host}:${porta}/http-bind`, // BOSH alternativo + clientNode: config.appId +}; +``` + +**Arquivo modificado:** +- `apps/web/src/lib/components/call/CallWindow.svelte` + +**Arquivo criado/atualizado:** +- `apps/web/src/lib/utils/jitsi.ts` - Adicionada função `obterHostEPorta()` + +--- + +## 2. Criação de Tracks Locais + +### Problema Identificado +- Os tracks locais não estavam sendo criados após entrar na conferência +- Faltava o evento `CONFERENCE_JOINED` para criar tracks locais + +### Correção Implementada +```typescript +conference.on(JitsiMeetJS.constants.events.conference.CONFERENCE_JOINED, async () => { + // Criar tracks locais com constraints apropriadas + const constraints = { + audio: estadoAtual.audioHabilitado ? { + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true + } : false, + video: estadoAtual.videoHabilitado ? { + facingMode: 'user', + width: { ideal: 1280 }, + height: { ideal: 720 } + } : false + }; + + const tracks = await JitsiMeetJS.createLocalTracks(constraints, { + devices: [], + cameraDeviceId: estadoChamada.dispositivos.cameraId || undefined, + micDeviceId: estadoChamada.dispositivos.microphoneId || undefined + }); + + // Adicionar tracks à conferência e anexar ao vídeo local + for (const track of tracks) { + await conference.addTrack(track); + if (track.getType() === 'video' && localVideo) { + track.attach(localVideo); + } + } +}); +``` + +**Arquivo modificado:** +- `apps/web/src/lib/components/call/CallWindow.svelte` + +--- + +## 3. Gerenciamento de Tracks + +### Problema Identificado +- Tracks locais não eram armazenados corretamente +- Falta de limpeza adequada ao finalizar chamada + +### Correção Implementada +- Adicionada variável de estado `localTracks: JitsiTrack[]` para rastrear todos os tracks locais +- Implementada limpeza adequada no método `finalizar()`: + - Desconectar tracks antes de liberar + - Dispor de todos os tracks locais + - Limpar referências + +**Arquivo modificado:** +- `apps/web/src/lib/components/call/CallWindow.svelte` + +--- + +## 4. Attach/Detach de Tracks Remotos + +### Problema Identificado +- Tracks remotos não eram anexados corretamente aos elementos de vídeo/áudio +- Não havia tratamento específico para áudio vs vídeo + +### Correção Implementada +```typescript +function adicionarTrackRemoto(track: JitsiTrack): void { + const participantId = track.getParticipantId(); + const trackType = track.getType(); + + if (trackType === 'audio') { + // Criar elemento de áudio invisível + const audioElement = document.createElement('audio'); + audioElement.id = `remote-audio-${participantId}`; + audioElement.autoplay = true; + track.attach(audioElement); + videoContainer.appendChild(audioElement); + } else if (trackType === 'video') { + // Criar elemento de vídeo + const videoElement = document.createElement('video'); + videoElement.id = `remote-video-${participantId}`; + videoElement.autoplay = true; + track.attach(videoElement); + videoContainer.appendChild(videoElement); + } +} +``` + +**Arquivo modificado:** +- `apps/web/src/lib/components/call/CallWindow.svelte` + +--- + +## 5. Controles de Áudio e Vídeo + +### Problema Identificado +- Os métodos `handleToggleAudio` e `handleToggleVideo` não criavam novos tracks quando necessário +- Não atualizavam corretamente o estado dos tracks locais + +### Correção Implementada +- Implementada lógica para criar tracks se não existirem +- Atualização correta do estado dos tracks (mute/unmute) +- Sincronização com o backend quando anfitrião +- Anexar/desanexar tracks ao vídeo local corretamente + +**Arquivo modificado:** +- `apps/web/src/lib/components/call/CallWindow.svelte` + +--- + +## 6. Tratamento de Erros + +### Problema Identificado +- Uso de `alert()` para erros (não amigável) +- Falta de mensagens de erro claras + +### Correção Implementada +- Implementado sistema de tratamento de erros com `ErrorModal` +- Integrado com `traduzirErro()` para mensagens amigáveis +- Adicionado estado de erro no componente: + ```typescript + let showErrorModal = $state(false); + let errorTitle = $state('Erro na Chamada'); + let errorMessage = $state(''); + let errorDetails = $state(undefined); + ``` + +**Arquivos modificados:** +- `apps/web/src/lib/components/call/CallWindow.svelte` +- Integração com `apps/web/src/lib/utils/erroHelpers.ts` + +--- + +## 7. Inicialização do Jitsi Meet JS + +### Problema Identificado +- Configuração básica do Jitsi pode estar incompleta +- Nível de log muito restritivo + +### Correção Implementada +```typescript +JitsiMeetJS.init({ + disableAudioLevels: false, // Habilitado para melhor qualidade + disableSimulcast: false, + enableWindowOnErrorHandler: true, + enableRemb: true, // REMB para controle de bitrate + enableTcc: true, // TCC para controle de congestionamento + disableThirdPartyRequests: false +}); + +JitsiMeetJS.setLogLevel(JitsiMeetJS.constants.logLevels.INFO); // Mais verboso para debug +``` + +**Arquivo modificado:** +- `apps/web/src/lib/components/call/CallWindow.svelte` + +--- + +## 8. UI/UX Melhorias + +### Implementado +- Indicador de conexão durante estabelecimento da chamada +- Mensagem de "Conectando..." enquanto não há conexão estabelecida +- Tratamento visual adequado de estados de conexão + +**Arquivo modificado:** +- `apps/web/src/lib/components/call/CallWindow.svelte` + +--- + +## 9. Eventos da Conferência + +### Adicionado +- `CONFERENCE_JOINED`: Criar tracks locais após entrar +- `CONFERENCE_LEFT`: Limpar tracks ao sair +- Melhor tratamento de `TRACK_ADDED` e `TRACK_REMOVED` + +**Arquivo modificado:** +- `apps/web/src/lib/components/call/CallWindow.svelte` + +--- + +## 10. Correção de Interfaces TypeScript + +### Adicionado +- Método `addTrack()` na interface `JitsiConference` +- Melhor tipagem de `JitsiTrack` com propriedade `track: MediaStreamTrack` + +**Arquivo modificado:** +- `apps/web/src/lib/components/call/CallWindow.svelte` + +--- + +## Configuração Necessária + +### Variáveis de Ambiente (.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=true +``` + +**Nota:** Para Docker Jitsi local, geralmente usa-se HTTPS na porta 8443. + +--- + +## Verificações Necessárias + +### 1. Docker Jitsi Rodando +```bash +docker ps | grep jitsi +``` + +### 2. Porta 8443 Acessível +```bash +curl -k https://localhost:8443 +``` + +### 3. Permissões do Navegador +- Microfone deve estar permitido +- Câmera deve estar permitida (para chamadas de vídeo) + +### 4. Logs do Navegador +- Abrir DevTools (F12) +- Verificar Console para erros de conexão +- Verificar Network para erros de rede + +--- + +## Próximos Passos (Se Necessário) + +1. **Testar conectividade** - Verificar se o servidor Jitsi responde corretamente +2. **Ajustar configuração de rede** - Se houver problemas de firewall ou CORS +3. **Configurar STUN/TURN** - Para conexões através de NAT (se necessário) +4. **Otimizar qualidade** - Ajustar bitrates e resoluções conforme necessário + +--- + +## Status + +✅ **Todas as correções foram implementadas** +✅ **Código sem erros de lint** +✅ **Tratamento de erros adequado** +✅ **Interfaces TypeScript corretas** +✅ **Gerenciamento de recursos adequado** + +--- + +**Data:** $(date) +**Versão:** 1.0.0 + diff --git a/PLANO_IMPLEMENTACAO_JITSI.md b/PLANO_IMPLEMENTACAO_JITSI.md new file mode 100644 index 0000000..4b05f03 --- /dev/null +++ b/PLANO_IMPLEMENTACAO_JITSI.md @@ -0,0 +1,701 @@ +# 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:** + +- `
%sveltekit.body%
diff --git a/apps/web/src/lib/components/ErrorModal.svelte b/apps/web/src/lib/components/ErrorModal.svelte index 5ccc653..a0a2bb4 100644 --- a/apps/web/src/lib/components/ErrorModal.svelte +++ b/apps/web/src/lib/components/ErrorModal.svelte @@ -1,5 +1,5 @@ {#if open} - e.target === e.currentTarget && handleClose()} + + {/if} + + diff --git a/apps/web/src/lib/components/Sidebar.svelte b/apps/web/src/lib/components/Sidebar.svelte index bc99b9f..22a158e 100644 --- a/apps/web/src/lib/components/Sidebar.svelte +++ b/apps/web/src/lib/components/Sidebar.svelte @@ -147,6 +147,9 @@ if (result.error) { console.error('Sign out error:', result.error); } + // Resetar tema para padrão ao fazer logout + const { aplicarTemaPadrao } = await import('$lib/utils/temas'); + aplicarTemaPadrao(); goto(resolve('/')); } @@ -158,8 +161,7 @@