Files
sgse-app/PLANO_IMPLEMENTACAO_JITSI.md
deyvisonwanderley 2792424454 feat: implement audio/video call functionality in chat
- 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.
2025-11-21 13:17:44 -03:00

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:

  1. Criar diretório para configuração Docker Jitsi:
mkdir -p ~/jitsi-docker
cd ~/jitsi-docker
  1. Clonar repositório oficial:
git clone https://github.com/jitsi/docker-jitsi-meet.git
cd docker-jitsi-meet
  1. Configurar variáveis de ambiente:
cp env.example .env
  1. Editar arquivo .env com 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
  1. Criar diretórios necessários:
mkdir -p ~/.jitsi-meet-cfg/{web/letsencrypt,transcripts,prosody/config,prosody/prosody-plugins-custom,jicofo,jvb}
  1. Iniciar containers:
docker-compose up -d
  1. Verificar status:
docker-compose ps
  1. Ver logs se necessário:
docker-compose logs -f
  1. Testar acesso:

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:

  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):

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-meet se 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 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):

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 áudio
  • iniciarGravacaoVideo(stream: MediaStream): MediaRecorder - Gravar áudio + vídeo
  • pararGravacao(recorder: MediaRecorder): Promise<Blob> - Parar e retornar blob
  • salvarGravacao(blob: Blob, nomeArquivo: string): void - Salvar localmente
  • obterDuracaoGravacao(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 arrastar
  • criarResizeHandler(element: HTMLElement, handles: HTMLElement[]): () => void - Criar handler de redimensionar
  • salvarPosicaoJanela(id: string, posicao: { x: number; y: number; width: number; height: number }): void - Salvar no localStorage
  • restaurarPosicaoJanela(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

  1. Etapa 1: Configurar Docker Jitsi (fora do código)
  2. Etapa 2: Atualizar schema com tabela chamadas
  3. Etapa 3: Criar backend chamadas.ts com todas as funções
  4. Etapa 4: Instalar dependências frontend
  5. Etapa 5: Configurar variáveis de ambiente
  6. Etapa 6: Criar utilitários Jitsi (jitsi.ts)
  7. Etapa 7: Criar store de chamadas (callStore.ts)
  8. Etapa 8: Criar utilitários de gravação (mediaRecorder.ts)
  9. Etapa 9: Criar CallWindow básico (apenas estrutura)
  10. Etapa 10: Integrar lib-jitsi-meet no CallWindow
  11. Etapa 11: Criar CallControls e integrar
  12. Etapa 12: Implementar contador de duração
  13. Etapa 13: Implementar janela flutuante (drag & resize)
  14. Etapa 14: Criar CallSettings e integração de dispositivos
  15. Etapa 15: Criar HostControls e lógica de anfitrião
  16. Etapa 16: Implementar gravação local
  17. Etapa 17: Criar RecordingIndicator
  18. Etapa 18: Integrar botões no ChatWindow
  19. Etapa 19: Testes completos
  20. 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

  1. Room Names: Gerar room names únicos usando conversaId + timestamp + hash
  2. Persistência: Salvar posição/tamanho da janela no localStorage
  3. Notificações: Notificar participantes quando chamada é criada/finalizada
  4. Limpeza: Sempre limpar recursos ao finalizar chamada
  5. Erros: Tratar erros de conexão, permissões de mídia, etc.
  6. 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


Data de Criação: 2025-01-XX
Versão: 1.0
Opção: Docker Local (Desenvolvimento)