feat: integrate Jitsi configuration and dynamic loading in CallWindow
- 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.
This commit is contained in:
@@ -153,12 +153,13 @@
|
||||
const chamadaQuery = useQuery(api.chamadas.obterChamada, { chamadaId });
|
||||
const chamada = $derived(chamadaQuery?.data);
|
||||
const meuPerfil = useQuery(api.auth.getCurrentUser, {});
|
||||
const configJitsiBackend = useQuery(api.configuracaoJitsi.obterConfigJitsi, {});
|
||||
|
||||
// Estado derivado do store
|
||||
const estadoChamada = $derived(get(callState));
|
||||
|
||||
// Configuração Jitsi
|
||||
const configJitsi = $derived.by(() => obterConfiguracaoJitsi());
|
||||
// Configuração Jitsi (busca do backend primeiro, depois fallback para env vars)
|
||||
const configJitsi = $derived.by(() => obterConfiguracaoJitsi(configJitsiBackend?.data || null));
|
||||
|
||||
// Handler de erro
|
||||
function handleError(message: string, details?: string): void {
|
||||
@@ -171,12 +172,137 @@
|
||||
}
|
||||
|
||||
// Carregar Jitsi dinamicamente
|
||||
// Polyfill para BlobBuilder (API antiga que lib-jitsi-meet pode usar)
|
||||
// Deve ser executado antes de qualquer import da biblioteca
|
||||
function adicionarBlobBuilderPolyfill(): void {
|
||||
if (!browser || typeof window === 'undefined') return;
|
||||
|
||||
// Verificar se já foi adicionado (evitar múltiplas execuções)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((window as any).__blobBuilderPolyfillAdded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Implementar BlobBuilder usando Blob moderno
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const BlobBuilderClass = class BlobBuilder {
|
||||
private parts: BlobPart[] = [];
|
||||
|
||||
append(data: BlobPart): void {
|
||||
this.parts.push(data);
|
||||
}
|
||||
|
||||
getBlob(contentType?: string): Blob {
|
||||
return new Blob(this.parts, contentType ? { type: contentType } : undefined);
|
||||
}
|
||||
};
|
||||
|
||||
// Adicionar em todos os possíveis locais onde a biblioteca pode procurar
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const win = window as any;
|
||||
|
||||
if (typeof win.BlobBuilder === 'undefined') {
|
||||
win.BlobBuilder = BlobBuilderClass;
|
||||
}
|
||||
|
||||
if (typeof win.WebKitBlobBuilder === 'undefined') {
|
||||
win.WebKitBlobBuilder = BlobBuilderClass;
|
||||
}
|
||||
|
||||
if (typeof win.MozBlobBuilder === 'undefined') {
|
||||
win.MozBlobBuilder = BlobBuilderClass;
|
||||
}
|
||||
|
||||
if (typeof win.MSBlobBuilder === 'undefined') {
|
||||
win.MSBlobBuilder = BlobBuilderClass;
|
||||
}
|
||||
|
||||
// Também adicionar no global scope caso a biblioteca procure lá
|
||||
if (typeof globalThis !== 'undefined') {
|
||||
if (typeof (globalThis as any).BlobBuilder === 'undefined') {
|
||||
(globalThis as any).BlobBuilder = BlobBuilderClass;
|
||||
}
|
||||
if (typeof (globalThis as any).WebKitBlobBuilder === 'undefined') {
|
||||
(globalThis as any).WebKitBlobBuilder = BlobBuilderClass;
|
||||
}
|
||||
}
|
||||
|
||||
// Marcar que o polyfill foi adicionado
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any).__blobBuilderPolyfillAdded = true;
|
||||
|
||||
console.log('✅ Polyfill BlobBuilder adicionado para todos os navegadores');
|
||||
}
|
||||
|
||||
// Executar polyfill imediatamente se estiver no browser
|
||||
// Isso garante que esteja disponível antes de qualquer import
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
adicionarBlobBuilderPolyfill();
|
||||
}
|
||||
|
||||
async function carregarJitsi(): Promise<void> {
|
||||
if (!browser || JitsiMeetJS) return;
|
||||
|
||||
try {
|
||||
console.log('🔄 Tentando carregar lib-jitsi-meet...');
|
||||
|
||||
// Adicionar polyfill antes de carregar a biblioteca
|
||||
adicionarBlobBuilderPolyfill();
|
||||
|
||||
// Tentar carregar o módulo lib-jitsi-meet dinamicamente
|
||||
// Usar import dinâmico para evitar problemas de SSR e permitir carregamento apenas no browser
|
||||
const module = await import('lib-jitsi-meet');
|
||||
JitsiMeetJS = module.default as unknown as JitsiMeetJSLib;
|
||||
|
||||
console.log('📦 Módulo carregado, verificando exportações...', {
|
||||
hasDefault: !!module.default,
|
||||
hasJitsiMeetJS: !!module.JitsiMeetJS,
|
||||
keys: Object.keys(module)
|
||||
});
|
||||
|
||||
// Tentar múltiplas formas de acessar o JitsiMeetJS
|
||||
// A biblioteca pode exportar de diferentes formas dependendo da versão
|
||||
let jitsiModule: unknown = null;
|
||||
|
||||
// Tentativa 1: export default
|
||||
if (module.default) {
|
||||
if (typeof module.default === 'object' && 'init' in module.default) {
|
||||
jitsiModule = module.default;
|
||||
console.log('✅ Encontrado em module.default');
|
||||
}
|
||||
}
|
||||
|
||||
// Tentativa 2: export nomeado JitsiMeetJS
|
||||
if (!jitsiModule && module.JitsiMeetJS) {
|
||||
jitsiModule = module.JitsiMeetJS;
|
||||
console.log('✅ Encontrado em module.JitsiMeetJS');
|
||||
}
|
||||
|
||||
// Tentativa 3: o próprio módulo pode ser o JitsiMeetJS
|
||||
if (!jitsiModule && typeof module === 'object' && 'init' in module) {
|
||||
jitsiModule = module;
|
||||
console.log('✅ Encontrado no próprio módulo');
|
||||
}
|
||||
|
||||
if (!jitsiModule) {
|
||||
throw new Error(
|
||||
'Não foi possível encontrar JitsiMeetJS no módulo. ' +
|
||||
'Verifique se lib-jitsi-meet está instalado corretamente.'
|
||||
);
|
||||
}
|
||||
|
||||
JitsiMeetJS = jitsiModule as unknown as JitsiMeetJSLib;
|
||||
|
||||
// Verificar se JitsiMeetJS foi inicializado corretamente
|
||||
if (!JitsiMeetJS || !JitsiMeetJS.init || typeof JitsiMeetJS.init !== 'function') {
|
||||
throw new Error('JitsiMeetJS não possui método init válido');
|
||||
}
|
||||
|
||||
// Verificar se JitsiConnection existe
|
||||
if (!JitsiMeetJS.JitsiConnection) {
|
||||
throw new Error('JitsiConnection não está disponível no módulo carregado');
|
||||
}
|
||||
|
||||
console.log('🔧 Inicializando Jitsi Meet JS...');
|
||||
|
||||
// Inicializar Jitsi
|
||||
JitsiMeetJS.init({
|
||||
@@ -188,16 +314,35 @@
|
||||
disableThirdPartyRequests: false
|
||||
});
|
||||
|
||||
// Configurar nível de log para DEBUG em desenvolvimento
|
||||
JitsiMeetJS.setLogLevel(JitsiMeetJS.constants.logLevels.INFO);
|
||||
// Configurar nível de log
|
||||
if (JitsiMeetJS.setLogLevel && typeof JitsiMeetJS.setLogLevel === 'function') {
|
||||
if (JitsiMeetJS.constants && JitsiMeetJS.constants.logLevels) {
|
||||
JitsiMeetJS.setLogLevel(JitsiMeetJS.constants.logLevels.INFO);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Jitsi Meet JS carregado e inicializado');
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar lib-jitsi-meet:', error);
|
||||
handleError(
|
||||
'Erro ao carregar biblioteca de vídeo',
|
||||
'Não foi possível carregar a biblioteca necessária para chamadas de vídeo. Por favor, recarregue a página.'
|
||||
);
|
||||
console.log('✅ Jitsi Meet JS carregado e inicializado com sucesso');
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('❌ Erro ao carregar lib-jitsi-meet:', error);
|
||||
console.error('Detalhes do erro:', {
|
||||
message: errorMessage,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
error
|
||||
});
|
||||
|
||||
// Verificar se é um erro de módulo não encontrado
|
||||
if (errorMessage.includes('Failed to fetch') || errorMessage.includes('Cannot find module')) {
|
||||
handleError(
|
||||
'Biblioteca de vídeo não encontrada',
|
||||
'A biblioteca Jitsi não pôde ser encontrada. Verifique se o pacote "lib-jitsi-meet" está instalado. Se o problema persistir, tente limpar o cache do navegador e recarregar a página.'
|
||||
);
|
||||
} else {
|
||||
handleError(
|
||||
'Erro ao carregar biblioteca de vídeo',
|
||||
`Não foi possível carregar a biblioteca necessária para chamadas de vídeo. Erro: ${errorMessage}. Por favor, recarregue a página e tente novamente.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -855,6 +1000,10 @@
|
||||
onMount(async () => {
|
||||
if (!browser) return;
|
||||
|
||||
// Adicionar polyfill BlobBuilder o mais cedo possível
|
||||
// Isso deve ser feito antes de qualquer tentativa de carregar lib-jitsi-meet
|
||||
adicionarBlobBuilderPolyfill();
|
||||
|
||||
// Inicializar store primeiro
|
||||
inicializarStore();
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface ConfiguracaoJitsi {
|
||||
appId: string;
|
||||
roomPrefix: string;
|
||||
useHttps: boolean;
|
||||
acceptSelfSignedCert?: boolean;
|
||||
}
|
||||
|
||||
export interface DispositivoMedia {
|
||||
@@ -22,9 +23,50 @@ export interface DispositivosDisponiveis {
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter configuração do Jitsi baseada em variáveis de ambiente
|
||||
* 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(): ConfiguracaoJitsi {
|
||||
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';
|
||||
@@ -49,9 +91,19 @@ export function obterHostEPorta(domain: string): { host: string; porta: number }
|
||||
|
||||
/**
|
||||
* 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'): string {
|
||||
const config = obterConfiguracaoJitsi();
|
||||
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);
|
||||
@@ -61,9 +113,18 @@ export function gerarRoomName(conversaId: string, tipo: 'audio' | 'video'): stri
|
||||
|
||||
/**
|
||||
* 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): string {
|
||||
const config = obterConfiguracaoJitsi();
|
||||
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}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user