|
|
|
|
@@ -6,8 +6,70 @@
|
|
|
|
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
|
|
|
|
import { X, GripVertical, GripHorizontal } from 'lucide-svelte';
|
|
|
|
|
|
|
|
|
|
// Tipos para Jitsi (evitando 'any')
|
|
|
|
|
interface JitsiConnection {
|
|
|
|
|
connect(): void;
|
|
|
|
|
disconnect(): void;
|
|
|
|
|
addEventListener(event: string, handler: (data?: unknown) => void): void;
|
|
|
|
|
initJitsiConference(roomName: string, options: Record<string, unknown>): JitsiConference;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface JitsiConference {
|
|
|
|
|
join(): void;
|
|
|
|
|
leave(): void;
|
|
|
|
|
on(event: string, handler: (...args: unknown[]) => void): void;
|
|
|
|
|
removeEventListener(event: string, handler: (...args: unknown[]) => void): void;
|
|
|
|
|
muteAudio(): void;
|
|
|
|
|
unmuteAudio(): void;
|
|
|
|
|
muteVideo(): void;
|
|
|
|
|
unmuteVideo(): void;
|
|
|
|
|
getParticipants(): Map<string, unknown>;
|
|
|
|
|
getLocalTracks(): JitsiTrack[];
|
|
|
|
|
setDisplayName(name: string): void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface JitsiTrack {
|
|
|
|
|
getType(): 'audio' | 'video';
|
|
|
|
|
isMuted(): boolean;
|
|
|
|
|
mute(): Promise<void>;
|
|
|
|
|
unmute(): Promise<void>;
|
|
|
|
|
attach(element: HTMLElement): void;
|
|
|
|
|
detach(element: HTMLElement): void;
|
|
|
|
|
dispose(): Promise<void>;
|
|
|
|
|
getParticipantId(): string;
|
|
|
|
|
track: MediaStreamTrack;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface JitsiMeetJSLib {
|
|
|
|
|
JitsiConnection: new (appId: string | null, token: string | null, options: Record<string, unknown>) => JitsiConnection;
|
|
|
|
|
constants: {
|
|
|
|
|
events: {
|
|
|
|
|
connection: {
|
|
|
|
|
CONNECTION_ESTABLISHED: string;
|
|
|
|
|
CONNECTION_FAILED: string;
|
|
|
|
|
CONNECTION_DISCONNECTED: string;
|
|
|
|
|
};
|
|
|
|
|
conference: {
|
|
|
|
|
USER_JOINED: string;
|
|
|
|
|
USER_LEFT: string;
|
|
|
|
|
TRACK_ADDED: string;
|
|
|
|
|
TRACK_REMOVED: string;
|
|
|
|
|
TRACK_MUTE_CHANGED: string;
|
|
|
|
|
CONFERENCE_JOINED: string;
|
|
|
|
|
CONFERENCE_LEFT: string;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
logLevels: {
|
|
|
|
|
ERROR: number;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
init(options: Record<string, unknown>): void;
|
|
|
|
|
setLogLevel(level: number): void;
|
|
|
|
|
createLocalTracks(options: Record<string, unknown>): Promise<JitsiTrack[]>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Importação dinâmica do Jitsi apenas no cliente
|
|
|
|
|
let JitsiMeetJS: any = $state(null);
|
|
|
|
|
let JitsiMeetJS: JitsiMeetJSLib | null = $state(null);
|
|
|
|
|
|
|
|
|
|
import CallControls from './CallControls.svelte';
|
|
|
|
|
import CallSettings from './CallSettings.svelte';
|
|
|
|
|
@@ -28,10 +90,11 @@
|
|
|
|
|
atualizarDispositivos,
|
|
|
|
|
setJitsiApi,
|
|
|
|
|
setStreamLocal,
|
|
|
|
|
finalizarChamada as finalizarChamadaStore
|
|
|
|
|
finalizarChamada as finalizarChamadaStore,
|
|
|
|
|
inicializarChamada
|
|
|
|
|
} from '$lib/stores/callStore';
|
|
|
|
|
|
|
|
|
|
import { obterConfiguracaoJitsi, gerarRoomName, obterUrlSala } from '$lib/utils/jitsi';
|
|
|
|
|
import { obterConfiguracaoJitsi } from '$lib/utils/jitsi';
|
|
|
|
|
import { GravadorMedia, gerarNomeArquivo, salvarGravacao } from '$lib/utils/mediaRecorder';
|
|
|
|
|
import {
|
|
|
|
|
criarDragHandler,
|
|
|
|
|
@@ -73,12 +136,13 @@
|
|
|
|
|
let duracaoTimer: ReturnType<typeof setInterval> | null = $state(null);
|
|
|
|
|
let gravador: GravadorMedia | null = $state(null);
|
|
|
|
|
|
|
|
|
|
let jitsiConnection: any = $state(null);
|
|
|
|
|
let jitsiConference: any = $state(null);
|
|
|
|
|
let jitsiConnection: JitsiConnection | null = $state(null);
|
|
|
|
|
let jitsiConference: JitsiConference | null = $state(null);
|
|
|
|
|
|
|
|
|
|
// Queries
|
|
|
|
|
const chamadaQuery = useQuery(api.chamadas.obterChamada, { chamadaId });
|
|
|
|
|
const chamada = $derived(chamadaQuery?.data);
|
|
|
|
|
const meuPerfil = useQuery(api.auth.getCurrentUser, {});
|
|
|
|
|
|
|
|
|
|
// Estado derivado do store
|
|
|
|
|
const estadoChamada = $derived(get(callState));
|
|
|
|
|
@@ -92,13 +156,41 @@
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const module = await import('lib-jitsi-meet');
|
|
|
|
|
JitsiMeetJS = module.default;
|
|
|
|
|
JitsiMeetJS = module.default as unknown as JitsiMeetJSLib;
|
|
|
|
|
|
|
|
|
|
// Inicializar Jitsi
|
|
|
|
|
JitsiMeetJS.init({
|
|
|
|
|
disableAudioLevels: true,
|
|
|
|
|
disableSimulcast: false,
|
|
|
|
|
enableWindowOnErrorHandler: true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
JitsiMeetJS.setLogLevel(JitsiMeetJS.constants.logLevels.ERROR);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Erro ao carregar lib-jitsi-meet:', error);
|
|
|
|
|
alert('Erro ao carregar biblioteca de vídeo');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Inicializar store
|
|
|
|
|
function inicializarStore(): void {
|
|
|
|
|
if (chamada && meuPerfil?.data) {
|
|
|
|
|
inicializarChamada(
|
|
|
|
|
chamadaId,
|
|
|
|
|
conversaId,
|
|
|
|
|
tipo,
|
|
|
|
|
roomName,
|
|
|
|
|
ehAnfitriao,
|
|
|
|
|
chamada.participantes.map(pId => ({
|
|
|
|
|
usuarioId: pId,
|
|
|
|
|
nome: 'Participante', // Será atualizado depois
|
|
|
|
|
audioHabilitado: true,
|
|
|
|
|
videoHabilitado: tipo === 'video'
|
|
|
|
|
}))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Inicializar Jitsi
|
|
|
|
|
async function inicializarJitsi(): Promise<void> {
|
|
|
|
|
if (!browser || !JitsiMeetJS) {
|
|
|
|
|
@@ -112,7 +204,7 @@
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const config = configJitsi();
|
|
|
|
|
const options: any = {
|
|
|
|
|
const options: Record<string, unknown> = {
|
|
|
|
|
hosts: {
|
|
|
|
|
domain: config.domain,
|
|
|
|
|
muc: `conference.${config.domain}`
|
|
|
|
|
@@ -123,9 +215,10 @@
|
|
|
|
|
|
|
|
|
|
const connection = new JitsiMeetJS.JitsiConnection(null, null, options);
|
|
|
|
|
jitsiConnection = connection;
|
|
|
|
|
setJitsiApi(connection);
|
|
|
|
|
|
|
|
|
|
// Eventos de conexão
|
|
|
|
|
connection.addEventListener(JitsiMeetJS.events.connection.CONNECTION_ESTABLISHED, () => {
|
|
|
|
|
connection.addEventListener(JitsiMeetJS.constants.events.connection.CONNECTION_ESTABLISHED, () => {
|
|
|
|
|
console.log('✅ Conexão estabelecida');
|
|
|
|
|
atualizarStatusConexao(true);
|
|
|
|
|
|
|
|
|
|
@@ -133,13 +226,15 @@
|
|
|
|
|
client.mutation(api.chamadas.iniciarChamada, { chamadaId });
|
|
|
|
|
|
|
|
|
|
// Criar conferência
|
|
|
|
|
const conferenceOptions: any = {
|
|
|
|
|
startAudioMuted: !estadoChamada.audioHabilitado,
|
|
|
|
|
startVideoMuted: !estadoChamada.videoHabilitado
|
|
|
|
|
const estadoAtual = get(callState);
|
|
|
|
|
const conferenceOptions: Record<string, unknown> = {
|
|
|
|
|
startAudioMuted: !estadoAtual.audioHabilitado,
|
|
|
|
|
startVideoMuted: !estadoAtual.videoHabilitado
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const conference = connection.initJitsiConference(roomName, conferenceOptions);
|
|
|
|
|
jitsiConference = conference;
|
|
|
|
|
setJitsiApi(conference);
|
|
|
|
|
|
|
|
|
|
// Eventos da conferência
|
|
|
|
|
configurarEventosConferencia(conference);
|
|
|
|
|
@@ -148,13 +243,13 @@
|
|
|
|
|
conference.join();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
connection.addEventListener(JitsiMeetJS.events.connection.CONNECTION_FAILED, (error) => {
|
|
|
|
|
connection.addEventListener(JitsiMeetJS.constants.events.connection.CONNECTION_FAILED, (error: unknown) => {
|
|
|
|
|
console.error('❌ Falha na conexão:', error);
|
|
|
|
|
atualizarStatusConexao(false);
|
|
|
|
|
alert('Erro ao conectar com o servidor de vídeo');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
connection.addEventListener(JitsiMeetJS.events.connection.CONNECTION_DISCONNECTED, () => {
|
|
|
|
|
connection.addEventListener(JitsiMeetJS.constants.events.connection.CONNECTION_DISCONNECTED, () => {
|
|
|
|
|
console.log('🔌 Conexão desconectada');
|
|
|
|
|
atualizarStatusConexao(false);
|
|
|
|
|
});
|
|
|
|
|
@@ -169,62 +264,64 @@
|
|
|
|
|
|
|
|
|
|
// Configurar eventos da conferência
|
|
|
|
|
function configurarEventosConferencia(
|
|
|
|
|
conference: any
|
|
|
|
|
conference: JitsiConference
|
|
|
|
|
): void {
|
|
|
|
|
if (!JitsiMeetJS) return;
|
|
|
|
|
|
|
|
|
|
// Participante entrou
|
|
|
|
|
conference.on(JitsiMeetJS.events.conference.USER_JOINED, (id: string, user: any) => {
|
|
|
|
|
conference.on(JitsiMeetJS.constants.events.conference.USER_JOINED, (id: unknown, user: unknown) => {
|
|
|
|
|
console.log('👤 Participante entrou:', id, user);
|
|
|
|
|
// Atualizar lista de participantes
|
|
|
|
|
atualizarListaParticipantes();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Participante saiu
|
|
|
|
|
conference.on(JitsiMeetJS.events.conference.USER_LEFT, (id: string) => {
|
|
|
|
|
conference.on(JitsiMeetJS.constants.events.conference.USER_LEFT, (id: unknown) => {
|
|
|
|
|
console.log('👋 Participante saiu:', id);
|
|
|
|
|
atualizarListaParticipantes();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Áudio mutado/desmutado
|
|
|
|
|
conference.on(JitsiMeetJS.events.conference.TRACK_MUTE_CHANGED, (track: any) => {
|
|
|
|
|
console.log('🎤 Mute mudou:', track);
|
|
|
|
|
if (track.getType() === 'audio') {
|
|
|
|
|
const participanteId = track.getParticipantId();
|
|
|
|
|
const isMuted = track.isMuted();
|
|
|
|
|
conference.on(JitsiMeetJS.constants.events.conference.TRACK_MUTE_CHANGED, (track: unknown) => {
|
|
|
|
|
const jitsiTrack = track as JitsiTrack;
|
|
|
|
|
console.log('🎤 Mute mudou:', jitsiTrack);
|
|
|
|
|
if (jitsiTrack.getType() === 'audio') {
|
|
|
|
|
// Atualizar estado do participante
|
|
|
|
|
atualizarListaParticipantes();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Vídeo mutado/desmutado
|
|
|
|
|
conference.on(JitsiMeetJS.events.conference.TRACK_MUTE_CHANGED, (track: any) => {
|
|
|
|
|
if (track.getType() === 'video') {
|
|
|
|
|
conference.on(JitsiMeetJS.constants.events.conference.TRACK_MUTE_CHANGED, (track: unknown) => {
|
|
|
|
|
const jitsiTrack = track as JitsiTrack;
|
|
|
|
|
if (jitsiTrack.getType() === 'video') {
|
|
|
|
|
atualizarListaParticipantes();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Novo track remoto
|
|
|
|
|
conference.on(
|
|
|
|
|
JitsiMeetJS.events.conference.TRACK_ADDED,
|
|
|
|
|
(track: any) => {
|
|
|
|
|
console.log('📹 Track adicionado:', track);
|
|
|
|
|
adicionarTrackRemoto(track);
|
|
|
|
|
JitsiMeetJS.constants.events.conference.TRACK_ADDED,
|
|
|
|
|
(track: unknown) => {
|
|
|
|
|
const jitsiTrack = track as JitsiTrack;
|
|
|
|
|
console.log('📹 Track adicionado:', jitsiTrack);
|
|
|
|
|
adicionarTrackRemoto(jitsiTrack);
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Track removido
|
|
|
|
|
conference.on(
|
|
|
|
|
JitsiMeetJS.events.conference.TRACK_REMOVED,
|
|
|
|
|
(track: any) => {
|
|
|
|
|
console.log('📹 Track removido:', track);
|
|
|
|
|
removerTrackRemoto(track);
|
|
|
|
|
JitsiMeetJS.constants.events.conference.TRACK_REMOVED,
|
|
|
|
|
(track: unknown) => {
|
|
|
|
|
const jitsiTrack = track as JitsiTrack;
|
|
|
|
|
console.log('📹 Track removido:', jitsiTrack);
|
|
|
|
|
removerTrackRemoto(jitsiTrack);
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Adicionar track remoto ao container
|
|
|
|
|
function adicionarTrackRemoto(track: any): void {
|
|
|
|
|
function adicionarTrackRemoto(track: JitsiTrack): void {
|
|
|
|
|
if (!videoContainer || track.getType() !== 'video') return;
|
|
|
|
|
|
|
|
|
|
const participantId = track.getParticipantId();
|
|
|
|
|
@@ -241,7 +338,7 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remover track remoto do container
|
|
|
|
|
function removerTrackRemoto(track: any): void {
|
|
|
|
|
function removerTrackRemoto(track: JitsiTrack): void {
|
|
|
|
|
if (!videoContainer) return;
|
|
|
|
|
const participantId = track.getParticipantId();
|
|
|
|
|
const videoElement = document.getElementById(`remote-video-${participantId}`);
|
|
|
|
|
@@ -257,12 +354,15 @@
|
|
|
|
|
const participants = jitsiConference.getParticipants();
|
|
|
|
|
// Mapear participantes para o formato esperado
|
|
|
|
|
// Isso pode ser expandido para buscar informações do backend
|
|
|
|
|
const participantesAtualizados = participants.map((p: any) => ({
|
|
|
|
|
usuarioId: p.getId() as Id<'usuarios'>,
|
|
|
|
|
nome: p.getDisplayName() || 'Participante',
|
|
|
|
|
audioHabilitado: !p.isAudioMuted(),
|
|
|
|
|
videoHabilitado: !p.isVideoMuted()
|
|
|
|
|
}));
|
|
|
|
|
const participantesAtualizados = Array.from(participants.values()).map((p: unknown) => {
|
|
|
|
|
const participant = p as { getId(): string; getDisplayName(): string; isAudioMuted(): boolean; isVideoMuted(): boolean };
|
|
|
|
|
return {
|
|
|
|
|
usuarioId: participant.getId() as Id<'usuarios'>,
|
|
|
|
|
nome: participant.getDisplayName() || 'Participante',
|
|
|
|
|
audioHabilitado: !participant.isAudioMuted(),
|
|
|
|
|
videoHabilitado: !participant.isVideoMuted()
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
atualizarParticipantes(participantesAtualizados);
|
|
|
|
|
}
|
|
|
|
|
@@ -303,7 +403,7 @@
|
|
|
|
|
|
|
|
|
|
// Criar MediaStream com todos os tracks
|
|
|
|
|
const stream = new MediaStream();
|
|
|
|
|
localTracks.forEach((track: any) => {
|
|
|
|
|
localTracks.forEach((track: JitsiTrack) => {
|
|
|
|
|
stream.addTrack(track.track);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@@ -475,7 +575,10 @@
|
|
|
|
|
onMount(async () => {
|
|
|
|
|
if (!browser) return;
|
|
|
|
|
|
|
|
|
|
// Carregar Jitsi primeiro
|
|
|
|
|
// Inicializar store primeiro
|
|
|
|
|
inicializarStore();
|
|
|
|
|
|
|
|
|
|
// Carregar Jitsi
|
|
|
|
|
await carregarJitsi();
|
|
|
|
|
|
|
|
|
|
// Configurar janela flutuante
|
|
|
|
|
|