feat: enhance call functionality and improve type safety

- Updated CallControls to replace the Record icon with Radio for better representation during recording states.
- Refactored CallWindow to introduce Jitsi connection and conference interfaces, improving type safety and clarity in handling Jitsi events.
- Streamlined event handling for connection and conference states, ensuring robust management of audio/video calls.
- Enhanced ChatWindow to directly import CallWindow, simplifying the component structure and improving call handling logic.
- Improved utility functions for window management to ensure compatibility with server-side rendering.
This commit is contained in:
2025-11-21 16:21:01 -03:00
parent c5e83464ba
commit 8fc3cf08c4
7 changed files with 194 additions and 70 deletions

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { Mic, MicOff, Video, VideoOff, Record, Square, Settings, PhoneOff, Circle } from 'lucide-svelte';
import { Mic, MicOff, Video, VideoOff, Radio, Square, Settings, PhoneOff, Circle } from 'lucide-svelte';
interface Props {
audioHabilitado: boolean;
@@ -101,7 +101,7 @@
{#if gravando}
<Square class="h-4 w-4" />
{:else}
<Record class="h-4 w-4" />
<Radio class="h-4 w-4 fill-current" />
{/if}
</button>
{/if}
@@ -130,3 +130,4 @@
</div>
</div>

View File

@@ -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

View File

@@ -110,3 +110,4 @@
</div>
</div>