- Added a new schema for managing audio/video calls, including fields for call type, room name, and participant management. - Enhanced ChatWindow component to support initiating audio and video calls with dynamic loading of the CallWindow component. - Updated package dependencies to include 'lib-jitsi-meet' for call handling. - Refactored existing code to accommodate new call features and improve user experience.
17 KiB
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:
- Criar diretório para configuração Docker Jitsi:
mkdir -p ~/jitsi-docker
cd ~/jitsi-docker
- Clonar repositório oficial:
git clone https://github.com/jitsi/docker-jitsi-meet.git
cd docker-jitsi-meet
- Configurar variáveis de ambiente:
cp env.example .env
- Editar arquivo
.envcom as seguintes configurações:
# 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
- Criar diretórios necessários:
mkdir -p ~/.jitsi-meet-cfg/{web/letsencrypt,transcripts,prosody/config,prosody/prosody-plugins-custom,jicofo,jvb}
- Iniciar containers:
docker-compose up -d
- Verificar status:
docker-compose ps
- Ver logs se necessário:
docker-compose logs -f
- 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:
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:
criarChamada- Criar nova chamadainiciarChamada- Marcar como em andamentofinalizarChamada- Finalizar e calcular duraçãoadicionarParticipante- Adicionar participanteremoverParticipante- Remover participantetoggleAudioVideo- Anfitrião controla áudio/vídeo de participanteatualizarConfiguracaoParticipante- Atualizar configuração individualiniciarGravacao- Marcar início de gravaçãofinalizarGravacao- Marcar fim de gravação
Queries:
obterChamadaAtiva- Buscar chamada ativa de uma conversalistarChamadas- Listar históricoverificarAnfitriao- Verificar se usuário é anfitriãoobterParticipantesChamada- Listar participantes
Tipos TypeScript (sem usar any):
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
cd apps/web
bun add lib-jitsi-meet
Dependências adicionais necessárias:
lib-jitsi-meet- Biblioteca oficial Jitsi- (Possivelmente tipos)
@types/lib-jitsi-meetse disponível
Etapa 5: Configurar Variáveis de Ambiente
Arquivo: apps/web/.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 salaobterConfiguracaoJitsi()- Retornar configuração do Jitsi baseada em .envvalidarDispositivos()- Validar disponibilidade de microfone/webcamobterDispositivosDisponiveis()- Listar dispositivos de mídia
Tipos (sem any):
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:
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 áudioiniciarGravacaoVideo(stream: MediaStream): MediaRecorder- Gravar áudio + vídeopararGravacao(recorder: MediaRecorder): Promise<Blob>- Parar e retornar blobsalvarGravacao(blob: Blob, nomeArquivo: string): void- Salvar localmenteobterDuracaoGravacao(recorder: MediaRecorder): number- Obter duração
Tipos:
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):
interface Props {
chamadaId: Id<'chamadas'>;
conversaId: Id<'conversas'>;
tipo: 'audio' | 'video';
roomName: string;
ehAnfitriao: boolean;
onClose: () => void;
}
Estrutura:
<script lang="ts">com tipos explícitos- Uso de
$state,$derived,$effect(Svelte 5) - Integração com
callStore - Eventos do Jitsi tratados tipados
Bibliotecas para janela flutuante:
- Usar eventos nativos de mouse/touch para drag
- CSS para redimensionamento com handles
- localStorage para persistir posição/tamanho
Etapa 10: Criar Componente CallControls
Arquivo: apps/web/src/lib/components/call/CallControls.svelte
Controles:
- Botão toggle áudio
- Botão toggle vídeo
- Botão gravação (se anfitrião)
- Botão configurações
- Botão encerrar chamada
- Contador de duração (HH:MM:SS)
Props:
interface Props {
audioHabilitado: boolean;
videoHabilitado: boolean;
gravando: boolean;
ehAnfitriao: boolean;
duracaoSegundos: number;
onToggleAudio: () => void;
onToggleVideo: () => void;
onIniciarGravacao: () => void;
onPararGravacao: () => void;
onAbrirConfiguracoes: () => void;
onEncerrar: () => void;
}
Etapa 11: Criar Componente CallSettings
Arquivo: apps/web/src/lib/components/call/CallSettings.svelte
Funcionalidades:
- Listar microfones disponíveis
- Listar webcams disponíveis
- Listar alto-falantes disponíveis
- Preview de vídeo antes de aplicar
- Teste de áudio
- Botões aplicar/cancelar
Props:
interface Props {
open: boolean;
dispositivoAtual: {
microphoneId: string | null;
cameraId: string | null;
speakerId: string | null;
};
onClose: () => void;
onAplicar: (dispositivos: {
microphoneId: string | null;
cameraId: string | null;
speakerId: string | null;
}) => void;
}
Etapa 12: Criar Componente HostControls
Arquivo: apps/web/src/lib/components/call/HostControls.svelte
Funcionalidades (apenas para anfitrião):
- Lista de participantes
- Toggle áudio por participante
- Toggle vídeo por participante
- Indicador visual de quem está gravando
- Status de cada participante
Props:
interface Props {
participantes: Array<{
usuarioId: Id<'usuarios'>;
nome: string;
avatar?: string;
audioHabilitado: boolean;
videoHabilitado: boolean;
forcadoPeloAnfitriao?: boolean;
}>;
onToggleParticipanteAudio: (usuarioId: Id<'usuarios'>) => void;
onToggleParticipanteVideo: (usuarioId: Id<'usuarios'>) => void;
}
Etapa 13: Criar Componente RecordingIndicator
Arquivo: apps/web/src/lib/components/call/RecordingIndicator.svelte
Características:
- Banner visível no topo da janela
- Ícone animado de gravação
- Mensagem clara de que está gravando
- Informação de quem iniciou (se disponível)
Props:
interface Props {
gravando: boolean;
iniciadoPor?: string; // Nome do usuário que iniciou
}
Etapa 14: Criar Utilitário de Janela Flutuante
Arquivo: apps/web/src/lib/utils/floatingWindow.ts
Funções:
criarDragHandler(element: HTMLElement, handle: HTMLElement): () => void- Criar handler de arrastarcriarResizeHandler(element: HTMLElement, handles: HTMLElement[]): () => void- Criar handler de redimensionarsalvarPosicaoJanela(id: string, posicao: { x: number; y: number; width: number; height: number }): void- Salvar no localStoragerestaurarPosicaoJanela(id: string): { x: number; y: number; width: number; height: number } | null- Restaurar do localStorage
Tipos:
interface PosicaoJanela {
x: number;
y: number;
width: number;
height: number;
}
interface LimitesJanela {
minWidth: number;
minHeight: number;
maxWidth?: number;
maxHeight?: number;
}
Etapa 15: Integrar com ChatWindow
Arquivo: apps/web/src/lib/components/chat/ChatWindow.svelte
Modificações:
- Adicionar botão de chamada de áudio
- Adicionar botão de chamada de vídeo
- Mostrar indicador quando há chamada ativa
- Importar e usar CallWindow quando houver chamada
Adicionar no topo (junto com outros botões):
<button
type="button"
class="btn btn-sm btn-circle"
onclick={() => iniciarChamada('audio')}
title="Ligação de áudio"
>
<Phone class="h-4 w-4" />
</button>
<button
type="button"
class="btn btn-sm btn-circle"
onclick={() => iniciarChamada('video')}
title="Ligação de vídeo"
>
<Video class="h-4 w-4" />
</button>
🔄 Ordem de Implementação Recomendada
- ✅ Etapa 1: Configurar Docker Jitsi (fora do código)
- ✅ Etapa 2: Atualizar schema com tabela chamadas
- ✅ Etapa 3: Criar backend chamadas.ts com todas as funções
- ✅ Etapa 4: Instalar dependências frontend
- ✅ Etapa 5: Configurar variáveis de ambiente
- ✅ Etapa 6: Criar utilitários Jitsi (jitsi.ts)
- ✅ Etapa 7: Criar store de chamadas (callStore.ts)
- ✅ Etapa 8: Criar utilitários de gravação (mediaRecorder.ts)
- ✅ Etapa 9: Criar CallWindow básico (apenas estrutura)
- ✅ Etapa 10: Integrar lib-jitsi-meet no CallWindow
- ✅ Etapa 11: Criar CallControls e integrar
- ✅ Etapa 12: Implementar contador de duração
- ✅ Etapa 13: Implementar janela flutuante (drag & resize)
- ✅ Etapa 14: Criar CallSettings e integração de dispositivos
- ✅ Etapa 15: Criar HostControls e lógica de anfitrião
- ✅ Etapa 16: Implementar gravação local
- ✅ Etapa 17: Criar RecordingIndicator
- ✅ Etapa 18: Integrar botões no ChatWindow
- ✅ Etapa 19: Testes completos
- ✅ Etapa 20: Ajustes finais e tratamento de erros
🛡️ Segurança e Boas Práticas
TypeScript
- ❌ NUNCA usar
any - ✅ Usar tipos explícitos em todas as funções
- ✅ Usar tipos inferidos do Convex quando possível
- ✅ Criar interfaces para objetos complexos
Svelte 5
- ✅ Usar
$props()para props - ✅ Usar
$state()para estado reativo - ✅ Usar
$derived()para valores derivados - ✅ Usar
$effect()para side effects
Validação
- ✅ Validar permissões no backend antes de mutações
- ✅ Validar entrada de dados
- ✅ Tratar erros adequadamente
- ✅ Logs de segurança (criação/finalização de chamadas)
Performance
- ✅ Cleanup adequado de event listeners
- ✅ Desconectar Jitsi ao fechar janela
- ✅ Parar gravação ao finalizar chamada
- ✅ Liberar streams de mídia
📝 Notas Importantes
- Room Names: Gerar room names únicos usando conversaId + timestamp + hash
- Persistência: Salvar posição/tamanho da janela no localStorage
- Notificações: Notificar participantes quando chamada é criada/finalizada
- Limpeza: Sempre limpar recursos ao finalizar chamada
- Erros: Tratar erros de conexão, permissões de mídia, etc.
- Acessibilidade: Adicionar labels, ARIA attributes, suporte a teclado
🧪 Testes
Testes Funcionais
- Criar chamada de áudio individual
- Criar chamada de vídeo individual
- Criar chamada em grupo
- Toggle áudio/vídeo
- Anfitrião controlar participantes
- Iniciar/parar gravação
- Contador de duração
- Configuração de dispositivos
- Janela flutuante drag/resize
Testes de Segurança
- Não anfitrião não pode controlar outros
- Não anfitrião não pode iniciar gravação
- Validação de participantes
- Rate limiting de criação de chamadas
Testes de Erros
- Conexão perdida
- Sem permissão de mídia
- Dispositivos não disponíveis
- Servidor Jitsi offline
📚 Referências
- Jitsi Meet Docker
- lib-jitsi-meet Documentation
- Svelte 5 Documentation
- Convex Documentation
- WebRTC API
- MediaRecorder API
Data de Criação: 2025-01-XX
Versão: 1.0
Opção: Docker Local (Desenvolvimento)