feat: implement audio/video call functionality in chat
- Added a new schema for managing audio/video calls, including fields for call type, room name, and participant management. - Enhanced ChatWindow component to support initiating audio and video calls with dynamic loading of the CallWindow component. - Updated package dependencies to include 'lib-jitsi-meet' for call handling. - Refactored existing code to accommodate new call features and improve user experience.
This commit is contained in:
322
apps/web/src/lib/stores/callStore.ts
Normal file
322
apps/web/src/lib/stores/callStore.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
/**
|
||||
* Store para gerenciar estado das chamadas de áudio/vídeo
|
||||
*/
|
||||
|
||||
import { writable, derived, get } from 'svelte/store';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
|
||||
export interface ParticipanteChamada {
|
||||
usuarioId: Id<'usuarios'>;
|
||||
nome: string;
|
||||
avatar?: string;
|
||||
audioHabilitado: boolean;
|
||||
videoHabilitado: boolean;
|
||||
forcadoPeloAnfitriao?: boolean;
|
||||
participantId?: string; // ID do participante no Jitsi
|
||||
}
|
||||
|
||||
export interface EstadoChamada {
|
||||
chamadaId: Id<'chamadas'> | null;
|
||||
conversaId: Id<'conversas'> | null;
|
||||
tipo: 'audio' | 'video' | null;
|
||||
roomName: string | null;
|
||||
estaConectado: boolean;
|
||||
audioHabilitado: boolean;
|
||||
videoHabilitado: boolean;
|
||||
gravando: boolean;
|
||||
ehAnfitriao: boolean;
|
||||
participantes: ParticipanteChamada[];
|
||||
duracaoSegundos: number;
|
||||
dispositivos: {
|
||||
microphoneId: string | null;
|
||||
cameraId: string | null;
|
||||
speakerId: string | null;
|
||||
};
|
||||
jitsiApi: any | null;
|
||||
streamLocal: MediaStream | null;
|
||||
}
|
||||
|
||||
const estadoInicial: EstadoChamada = {
|
||||
chamadaId: null,
|
||||
conversaId: null,
|
||||
tipo: null,
|
||||
roomName: null,
|
||||
estaConectado: false,
|
||||
audioHabilitado: true,
|
||||
videoHabilitado: false,
|
||||
gravando: false,
|
||||
ehAnfitriao: false,
|
||||
participantes: [],
|
||||
duracaoSegundos: 0,
|
||||
dispositivos: {
|
||||
microphoneId: null,
|
||||
cameraId: null,
|
||||
speakerId: null
|
||||
},
|
||||
jitsiApi: null,
|
||||
streamLocal: null
|
||||
};
|
||||
|
||||
// Store principal do estado da chamada
|
||||
export const callState = writable<EstadoChamada>(estadoInicial);
|
||||
|
||||
// Store para indicar se há chamada ativa
|
||||
export const chamadaAtiva = derived(
|
||||
callState,
|
||||
($state) => $state.chamadaId !== null
|
||||
);
|
||||
|
||||
// Store para indicar se está conectado
|
||||
export const estaConectado = derived(
|
||||
callState,
|
||||
($state) => $state.estaConectado
|
||||
);
|
||||
|
||||
// Store para indicar se está gravando
|
||||
export const gravando = derived(
|
||||
callState,
|
||||
($state) => $state.gravando
|
||||
);
|
||||
|
||||
// Funções para atualizar o estado
|
||||
|
||||
/**
|
||||
* Inicializar chamada
|
||||
*/
|
||||
export function inicializarChamada(
|
||||
chamadaId: Id<'chamadas'>,
|
||||
conversaId: Id<'conversas'>,
|
||||
tipo: 'audio' | 'video',
|
||||
roomName: string,
|
||||
ehAnfitriao: boolean,
|
||||
participantes: ParticipanteChamada[]
|
||||
): void {
|
||||
callState.set({
|
||||
...estadoInicial,
|
||||
chamadaId,
|
||||
conversaId,
|
||||
tipo,
|
||||
roomName,
|
||||
ehAnfitriao,
|
||||
participantes,
|
||||
videoHabilitado: tipo === 'video'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizar chamada e limpar estado
|
||||
*/
|
||||
export function finalizarChamada(): void {
|
||||
const estadoAtual = get(callState);
|
||||
|
||||
// Liberar recursos
|
||||
if (estadoAtual.streamLocal) {
|
||||
estadoAtual.streamLocal.getTracks().forEach((track) => track.stop());
|
||||
}
|
||||
|
||||
callState.set(estadoInicial);
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualizar status de conexão
|
||||
*/
|
||||
export function atualizarStatusConexao(estaConectado: boolean): void {
|
||||
callState.update((state) => ({
|
||||
...state,
|
||||
estaConectado
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle áudio
|
||||
*/
|
||||
export function toggleAudio(): void {
|
||||
callState.update((state) => ({
|
||||
...state,
|
||||
audioHabilitado: !state.audioHabilitado
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle vídeo
|
||||
*/
|
||||
export function toggleVideo(): void {
|
||||
callState.update((state) => ({
|
||||
...state,
|
||||
videoHabilitado: !state.videoHabilitado
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Definir áudio habilitado/desabilitado
|
||||
*/
|
||||
export function setAudioHabilitado(habilitado: boolean): void {
|
||||
callState.update((state) => ({
|
||||
...state,
|
||||
audioHabilitado: habilitado
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Definir vídeo habilitado/desabilitado
|
||||
*/
|
||||
export function setVideoHabilitado(habilitado: boolean): void {
|
||||
callState.update((state) => ({
|
||||
...state,
|
||||
videoHabilitado: habilitado
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualizar lista de participantes
|
||||
*/
|
||||
export function atualizarParticipantes(participantes: ParticipanteChamada[]): void {
|
||||
callState.update((state) => ({
|
||||
...state,
|
||||
participantes
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adicionar participante
|
||||
*/
|
||||
export function adicionarParticipante(participante: ParticipanteChamada): void {
|
||||
callState.update((state) => {
|
||||
// Verificar se já existe
|
||||
const existe = state.participantes.some(
|
||||
(p) => p.usuarioId === participante.usuarioId
|
||||
);
|
||||
|
||||
if (existe) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
participantes: [...state.participantes, participante]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remover participante
|
||||
*/
|
||||
export function removerParticipante(usuarioId: Id<'usuarios'>): void {
|
||||
callState.update((state) => ({
|
||||
...state,
|
||||
participantes: state.participantes.filter(
|
||||
(p) => p.usuarioId !== usuarioId
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualizar status de áudio/vídeo de participante
|
||||
*/
|
||||
export function atualizarParticipanteMidia(
|
||||
usuarioId: Id<'usuarios'>,
|
||||
audioHabilitado?: boolean,
|
||||
videoHabilitado?: boolean
|
||||
): void {
|
||||
callState.update((state) => ({
|
||||
...state,
|
||||
participantes: state.participantes.map((p) =>
|
||||
p.usuarioId === usuarioId
|
||||
? {
|
||||
...p,
|
||||
audioHabilitado: audioHabilitado ?? p.audioHabilitado,
|
||||
videoHabilitado: videoHabilitado ?? p.videoHabilitado
|
||||
}
|
||||
: p
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Iniciar gravação
|
||||
*/
|
||||
export function iniciarGravacao(): void {
|
||||
callState.update((state) => ({
|
||||
...state,
|
||||
gravando: true
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parar gravação
|
||||
*/
|
||||
export function pararGravacao(): void {
|
||||
callState.update((state) => ({
|
||||
...state,
|
||||
gravando: false
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualizar duração da chamada
|
||||
*/
|
||||
export function atualizarDuracao(segundos: number): void {
|
||||
callState.update((state) => ({
|
||||
...state,
|
||||
duracaoSegundos: segundos
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualizar dispositivos selecionados
|
||||
*/
|
||||
export function atualizarDispositivos(dispositivos: {
|
||||
microphoneId?: string | null;
|
||||
cameraId?: string | null;
|
||||
speakerId?: string | null;
|
||||
}): void {
|
||||
callState.update((state) => ({
|
||||
...state,
|
||||
dispositivos: {
|
||||
...state.dispositivos,
|
||||
...dispositivos
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Definir API Jitsi
|
||||
*/
|
||||
export function setJitsiApi(api: any | null): void {
|
||||
callState.update((state) => ({
|
||||
...state,
|
||||
jitsiApi: api
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Definir stream local
|
||||
*/
|
||||
export function setStreamLocal(stream: MediaStream | null): void {
|
||||
callState.update((state) => {
|
||||
// Parar stream anterior se existir
|
||||
if (state.streamLocal) {
|
||||
state.streamLocal.getTracks().forEach((track) => track.stop());
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
streamLocal: stream
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter estado atual (helper)
|
||||
*/
|
||||
export function obterEstadoAtual(): EstadoChamada {
|
||||
return get(callState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resetar estado (para cleanup)
|
||||
*/
|
||||
export function resetarEstado(): void {
|
||||
finalizarChamada();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user