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:
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -110,3 +110,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
import UserAvatar from './UserAvatar.svelte';
|
||||
import ScheduleMessageModal from './ScheduleMessageModal.svelte';
|
||||
import SalaReuniaoManager from './SalaReuniaoManager.svelte';
|
||||
import CallWindow from '../call/CallWindow.svelte';
|
||||
import { getAvatarUrl } from '$lib/utils/avatarGenerator';
|
||||
import { browser } from '$app/environment';
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
Bell,
|
||||
X,
|
||||
@@ -40,19 +40,17 @@
|
||||
let showAdminMenu = $state(false);
|
||||
let showNotificacaoModal = $state(false);
|
||||
let iniciandoChamada = $state(false);
|
||||
let chamadaAtiva = $state<Id<'chamadas'> | null>(null);
|
||||
|
||||
// Importação dinâmica do CallWindow apenas no cliente
|
||||
let CallWindowComponent: any = $state(null);
|
||||
|
||||
const chamadaAtivaQuery = useQuery(api.chamadas.obterChamadaAtiva, {
|
||||
conversaId: conversaId as Id<'conversas'>
|
||||
});
|
||||
const chamadaAtual = $derived(chamadaAtivaQuery?.data);
|
||||
|
||||
const conversas = useQuery(api.chat.listarConversas, {});
|
||||
const isAdmin = useQuery(api.chat.verificarSeEhAdmin, {
|
||||
conversaId: conversaId as Id<'conversas'>
|
||||
});
|
||||
const chamadaAtivaQuery = useQuery(api.chamadas.obterChamadaAtiva, {
|
||||
conversaId: conversaId as Id<'conversas'>
|
||||
});
|
||||
|
||||
const conversa = $derived(() => {
|
||||
console.log('🔍 [ChatWindow] Buscando conversa ID:', conversaId);
|
||||
@@ -168,17 +166,6 @@
|
||||
chamadaAtiva = null;
|
||||
}
|
||||
|
||||
// Carregar CallWindow dinamicamente apenas no cliente
|
||||
onMount(async () => {
|
||||
if (browser && !CallWindowComponent) {
|
||||
try {
|
||||
const module = await import('../call/CallWindow.svelte');
|
||||
CallWindowComponent = module.default;
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar CallWindow:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Verificar se usuário é anfitrião da chamada atual
|
||||
const meuPerfil = useQuery(api.auth.getCurrentUser, {});
|
||||
@@ -501,10 +488,9 @@
|
||||
{/if}
|
||||
|
||||
<!-- Janela de Chamada -->
|
||||
{#if browser && chamadaAtiva && chamadaAtual && CallWindowComponent}
|
||||
{@const Component = CallWindowComponent}
|
||||
{#if browser && chamadaAtiva && chamadaAtual}
|
||||
<div class="pointer-events-none fixed inset-0 z-[9999]">
|
||||
<Component
|
||||
<CallWindow
|
||||
chamadaId={chamadaAtiva}
|
||||
conversaId={conversaId as Id<'conversas'>}
|
||||
tipo={chamadaAtual.tipo}
|
||||
|
||||
@@ -16,12 +16,24 @@ export interface LimitesJanela {
|
||||
maxHeight?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_LIMITS: LimitesJanela = {
|
||||
minWidth: 400,
|
||||
minHeight: 300,
|
||||
maxWidth: window.innerWidth,
|
||||
maxHeight: window.innerHeight
|
||||
};
|
||||
function getDefaultLimits(): LimitesJanela {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
minWidth: 400,
|
||||
minHeight: 300,
|
||||
maxWidth: 1920,
|
||||
maxHeight: 1080
|
||||
};
|
||||
}
|
||||
return {
|
||||
minWidth: 400,
|
||||
minHeight: 300,
|
||||
maxWidth: window.innerWidth,
|
||||
maxHeight: window.innerHeight
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_LIMITS: LimitesJanela = getDefaultLimits();
|
||||
|
||||
/**
|
||||
* Salvar posição da janela no localStorage
|
||||
@@ -30,6 +42,9 @@ export function salvarPosicaoJanela(
|
||||
id: string,
|
||||
posicao: PosicaoJanela
|
||||
): void {
|
||||
if (typeof window === 'undefined' || typeof localStorage === 'undefined') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const key = `floating-window-${id}`;
|
||||
localStorage.setItem(key, JSON.stringify(posicao));
|
||||
@@ -42,6 +57,9 @@ export function salvarPosicaoJanela(
|
||||
* Restaurar posição da janela do localStorage
|
||||
*/
|
||||
export function restaurarPosicaoJanela(id: string): PosicaoJanela | null {
|
||||
if (typeof window === 'undefined' || typeof localStorage === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const key = `floating-window-${id}`;
|
||||
const saved = localStorage.getItem(key);
|
||||
@@ -75,6 +93,14 @@ export function obterPosicaoInicial(
|
||||
width: number = 800,
|
||||
height: number = 600
|
||||
): PosicaoJanela {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
x: 100,
|
||||
y: 100,
|
||||
width,
|
||||
height
|
||||
};
|
||||
}
|
||||
return {
|
||||
x: (window.innerWidth - width) / 2,
|
||||
y: (window.innerHeight - height) / 2,
|
||||
@@ -124,6 +150,7 @@ export function criarDragHandler(
|
||||
let newY = initialY + deltaY;
|
||||
|
||||
// Limitar movimento dentro da tela
|
||||
if (typeof window === 'undefined') return;
|
||||
const maxX = window.innerWidth - element.offsetWidth;
|
||||
const maxY = window.innerHeight - element.offsetHeight;
|
||||
|
||||
@@ -173,6 +200,7 @@ export function criarDragHandler(
|
||||
let newX = initialX + deltaX;
|
||||
let newY = initialY + deltaY;
|
||||
|
||||
if (typeof window === 'undefined') return;
|
||||
const maxX = window.innerWidth - element.offsetWidth;
|
||||
const maxY = window.innerHeight - element.offsetHeight;
|
||||
|
||||
@@ -306,6 +334,8 @@ export function criarResizeHandler(
|
||||
newTop = startTop + deltaY;
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
// Aplicar limites
|
||||
const maxWidth = limites.maxWidth || window.innerWidth - newLeft;
|
||||
const maxHeight = limites.maxHeight || window.innerHeight - newTop;
|
||||
@@ -364,3 +394,4 @@ export function criarResizeHandler(
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -329,3 +329,4 @@ export class GravadorMedia {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -576,3 +576,4 @@ export const obterChamada = query({
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user