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.
This commit is contained in:
2025-11-21 13:17:44 -03:00
parent bc1e08914b
commit 2792424454
15 changed files with 3986 additions and 3 deletions

View File

@@ -0,0 +1,265 @@
/**
* Utilitários para integração com Jitsi Meet
*/
export interface ConfiguracaoJitsi {
domain: string;
appId: string;
roomPrefix: string;
useHttps: 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 baseada em variáveis de ambiente
*/
export function obterConfiguracaoJitsi(): 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';
return {
domain,
appId,
roomPrefix,
useHttps
};
}
/**
* Gerar nome único para a sala Jitsi
*/
export function gerarRoomName(conversaId: string, tipo: 'audio' | 'video'): string {
const config = obterConfiguracaoJitsi();
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
*/
export function obterUrlSala(roomName: string): string {
const config = obterConfiguracaoJitsi();
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<MediaStream | null> {
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<DispositivosDisponiveis> {
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<boolean> {
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
};
}