/** * Utilitários para integração com Jitsi Meet */ export interface ConfiguracaoJitsi { domain: string; appId: string; roomPrefix: string; useHttps: boolean; acceptSelfSignedCert?: boolean; } export interface DispositivoMedia { deviceId: string; label: string; kind: 'audioinput' | 'audiooutput' | 'videoinput'; } export interface DispositivosDisponiveis { microphones: DispositivoMedia[]; speakers: DispositivoMedia[]; cameras: DispositivoMedia[]; } /** * Obter configuração do Jitsi do backend ou variáveis de ambiente (fallback) * * @param configBackend - Configuração do backend (opcional). Se fornecida, será usada. * @returns Configuração do Jitsi */ export function obterConfiguracaoJitsi(configBackend?: { domain: string; appId: string; roomPrefix: string; useHttps: boolean; acceptSelfSignedCert?: boolean; } | null): ConfiguracaoJitsi { // Se há configuração do backend e está ativa, usar ela if (configBackend) { return { domain: configBackend.domain, appId: configBackend.appId, roomPrefix: configBackend.roomPrefix, useHttps: configBackend.useHttps, acceptSelfSignedCert: configBackend.acceptSelfSignedCert || false }; } // Fallback para variáveis de ambiente const domain = import.meta.env.VITE_JITSI_DOMAIN || 'localhost:8443'; const appId = import.meta.env.VITE_JITSI_APP_ID || 'sgse-app'; const roomPrefix = import.meta.env.VITE_JITSI_ROOM_PREFIX || 'sgse'; const useHttps = import.meta.env.VITE_JITSI_USE_HTTPS === 'true' || domain.includes(':8443'); const acceptSelfSignedCert = import.meta.env.VITE_JITSI_ACCEPT_SELF_SIGNED === 'true'; return { domain, appId, roomPrefix, useHttps, acceptSelfSignedCert }; } /** * Obter configuração do Jitsi de forma síncrona (apenas variáveis de ambiente) * Use esta função quando não houver acesso ao Convex client */ export function obterConfiguracaoJitsiSync(): ConfiguracaoJitsi { const domain = import.meta.env.VITE_JITSI_DOMAIN || 'localhost:8443'; const appId = import.meta.env.VITE_JITSI_APP_ID || 'sgse-app'; const roomPrefix = import.meta.env.VITE_JITSI_ROOM_PREFIX || 'sgse'; const useHttps = import.meta.env.VITE_JITSI_USE_HTTPS === 'true' || domain.includes(':8443'); return { domain, appId, roomPrefix, useHttps }; } /** * Obter host e porta separados do domínio */ export function obterHostEPorta(domain: string): { host: string; porta: number } { const [host, portaStr] = domain.split(':'); const porta = portaStr ? parseInt(portaStr, 10) : (domain.includes('8443') ? 8443 : 443); return { host: host || 'localhost', porta }; } /** * Gerar nome único para a sala Jitsi * * @param conversaId - ID da conversa * @param tipo - Tipo de chamada ('audio' ou 'video') * @param configBackend - Configuração do backend (opcional). Se não fornecida, usa fallback. */ export function gerarRoomName( conversaId: string, tipo: 'audio' | 'video', configBackend?: { roomPrefix: string; } | null ): string { const config = obterConfiguracaoJitsi(configBackend || undefined); const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 9); const conversaHash = conversaId.replace(/[^a-zA-Z0-9]/g, '').substring(0, 10); return `${config.roomPrefix}-${tipo}-${conversaHash}-${timestamp}-${random}`; } /** * Obter URL completa da sala Jitsi * * @param roomName - Nome da sala Jitsi * @param configBackend - Configuração do backend (opcional). Se não fornecida, usa fallback. */ export function obterUrlSala( roomName: string, configBackend?: { domain: string; useHttps: boolean; } | null ): string { const config = obterConfiguracaoJitsi(configBackend || undefined); const protocol = config.useHttps ? 'https' : 'http'; return `${protocol}://${config.domain}/${roomName}`; } /** * Validar se dispositivos de mídia estão disponíveis */ export async function validarDispositivos(): Promise<{ microfoneDisponivel: boolean; cameraDisponivel: boolean; }> { if (typeof window === 'undefined') { return { microfoneDisponivel: false, cameraDisponivel: false }; } try { const devices = await navigator.mediaDevices.enumerateDevices(); const microfoneDisponivel = devices.some( (device) => device.kind === 'audioinput' ); const cameraDisponivel = devices.some( (device) => device.kind === 'videoinput' ); return { microfoneDisponivel, cameraDisponivel }; } catch (error) { console.error('Erro ao validar dispositivos:', error); return { microfoneDisponivel: false, cameraDisponivel: false }; } } /** * Solicitar permissão de acesso aos dispositivos de mídia */ export async function solicitarPermissaoMidia( audio: boolean = true, video: boolean = false ): Promise { if (typeof window === 'undefined') { return null; } try { const stream = await navigator.mediaDevices.getUserMedia({ audio, video: video ? { facingMode: 'user' } : false }); return stream; } catch (error) { console.error('Erro ao solicitar permissão de mídia:', error); return null; } } /** * Obter lista de dispositivos de mídia disponíveis */ export async function obterDispositivosDisponiveis(): Promise { if (typeof window === 'undefined') { return { microphones: [], speakers: [], cameras: [] }; } try { // Solicitar permissão primeiro para obter labels dos dispositivos await navigator.mediaDevices.getUserMedia({ audio: true, video: true }); const devices = await navigator.mediaDevices.enumerateDevices(); const microphones: DispositivoMedia[] = devices .filter((device) => device.kind === 'audioinput') .map((device) => ({ deviceId: device.deviceId, label: device.label || `Microfone ${device.deviceId.substring(0, 8)}`, kind: 'audioinput' as const })); const speakers: DispositivoMedia[] = devices .filter((device) => device.kind === 'audiooutput') .map((device) => ({ deviceId: device.deviceId, label: device.label || `Alto-falante ${device.deviceId.substring(0, 8)}`, kind: 'audiooutput' as const })); const cameras: DispositivoMedia[] = devices .filter((device) => device.kind === 'videoinput') .map((device) => ({ deviceId: device.deviceId, label: device.label || `Câmera ${device.deviceId.substring(0, 8)}`, kind: 'videoinput' as const })); return { microphones, speakers, cameras }; } catch (error) { console.error('Erro ao obter dispositivos disponíveis:', error); return { microphones: [], speakers: [], cameras: [] }; } } /** * Configurar dispositivo de áudio de saída (alto-falante) */ export async function configurarAltoFalante( deviceId: string, audioElement: HTMLAudioElement ): Promise { if (typeof window === 'undefined') { return false; } try { // @ts-expect-error - setSinkId pode não estar disponível em todos os navegadores if (audioElement.setSinkId && typeof audioElement.setSinkId === 'function') { await audioElement.setSinkId(deviceId); return true; } return false; } catch (error) { console.error('Erro ao configurar alto-falante:', error); return false; } } /** * Verificar se WebRTC está disponível no navegador */ export function verificarSuporteWebRTC(): boolean { if (typeof window === 'undefined') { return false; } return !!( navigator.mediaDevices && navigator.mediaDevices.getUserMedia && window.RTCPeerConnection ); } /** * Obter informações do navegador para debug */ export function obterInfoNavegador(): { navegador: string; versao: string; webrtcSuportado: boolean; mediaDevicesDisponivel: boolean; } { if (typeof window === 'undefined') { return { navegador: 'Servidor', versao: 'N/A', webrtcSuportado: false, mediaDevicesDisponivel: false }; } const userAgent = navigator.userAgent; let navegador = 'Desconhecido'; let versao = 'Desconhecida'; if (userAgent.indexOf('Chrome') > -1) { navegador = 'Chrome'; const match = userAgent.match(/Chrome\/(\d+)/); versao = match ? match[1] : 'Desconhecida'; } else if (userAgent.indexOf('Firefox') > -1) { navegador = 'Firefox'; const match = userAgent.match(/Firefox\/(\d+)/); versao = match ? match[1] : 'Desconhecida'; } else if (userAgent.indexOf('Safari') > -1) { navegador = 'Safari'; const match = userAgent.match(/Version\/(\d+)/); versao = match ? match[1] : 'Desconhecida'; } else if (userAgent.indexOf('Edge') > -1) { navegador = 'Edge'; const match = userAgent.match(/Edge\/(\d+)/); versao = match ? match[1] : 'Desconhecida'; } return { navegador, versao, webrtcSuportado: verificarSuporteWebRTC(), mediaDevicesDisponivel: !!navigator.mediaDevices }; }