- Added support for Jitsi configuration retrieval from the backend, allowing for dynamic room name generation based on the active configuration. - Implemented a polyfill for BlobBuilder to ensure compatibility with the lib-jitsi-meet library across different browsers. - Enhanced error handling during the loading of the Jitsi library, providing clearer feedback for missing modules and connection issues. - Updated Vite configuration to exclude lib-jitsi-meet from SSR and allow dynamic loading in the browser. - Introduced a new route for Jitsi settings in the dashboard for user configuration of Jitsi Meet parameters.
336 lines
8.6 KiB
TypeScript
336 lines
8.6 KiB
TypeScript
/**
|
|
* 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<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
|
|
};
|
|
}
|
|
|