302 lines
8.2 KiB
Svelte
302 lines
8.2 KiB
Svelte
<script lang="ts">
|
|
import { Check, Volume2, X } from 'lucide-svelte';
|
|
import { onMount } from 'svelte';
|
|
import type { DispositivoMedia } from '$lib/utils/jitsi';
|
|
import { obterDispositivosDisponiveis, solicitarPermissaoMidia } from '$lib/utils/jitsi';
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
dispositivoAtual: {
|
|
microphoneId: string | null;
|
|
cameraId: string | null;
|
|
speakerId: string | null;
|
|
};
|
|
onClose: () => void;
|
|
onAplicar: (dispositivos: {
|
|
microphoneId: string | null;
|
|
cameraId: string | null;
|
|
speakerId: string | null;
|
|
}) => void;
|
|
}
|
|
|
|
let { open, dispositivoAtual, onClose, onAplicar }: Props = $props();
|
|
|
|
let dispositivos = $state<{
|
|
microphones: DispositivoMedia[];
|
|
speakers: DispositivoMedia[];
|
|
cameras: DispositivoMedia[];
|
|
}>({
|
|
microphones: [],
|
|
speakers: [],
|
|
cameras: []
|
|
});
|
|
|
|
let selecionados = $state({
|
|
microphoneId: dispositivoAtual.microphoneId || null,
|
|
cameraId: dispositivoAtual.cameraId || null,
|
|
speakerId: dispositivoAtual.speakerId || null
|
|
});
|
|
|
|
let carregando = $state(false);
|
|
let previewStream: MediaStream | null = $state(null);
|
|
let previewVideo: HTMLVideoElement | null = $state(null);
|
|
let erro = $state<string | null>(null);
|
|
|
|
// Carregar dispositivos disponíveis
|
|
async function carregarDispositivos(): Promise<void> {
|
|
carregando = true;
|
|
erro = null;
|
|
try {
|
|
dispositivos = await obterDispositivosDisponiveis();
|
|
if (dispositivos.microphones.length === 0 && dispositivos.cameras.length === 0) {
|
|
erro = 'Nenhum dispositivo de mídia encontrado. Verifique as permissões do navegador.';
|
|
}
|
|
} catch (error) {
|
|
console.error('Erro ao carregar dispositivos:', error);
|
|
erro = 'Erro ao carregar dispositivos de mídia.';
|
|
} finally {
|
|
carregando = false;
|
|
}
|
|
}
|
|
|
|
// Atualizar preview quando mudar dispositivos
|
|
async function atualizarPreview(): Promise<void> {
|
|
if (previewStream) {
|
|
previewStream.getTracks().forEach((track) => track.stop());
|
|
previewStream = null;
|
|
}
|
|
|
|
if (!previewVideo) return;
|
|
|
|
try {
|
|
const audio = selecionados.microphoneId !== null;
|
|
const video = selecionados.cameraId !== null;
|
|
|
|
if (audio || video) {
|
|
const constraints: MediaStreamConstraints = {
|
|
audio: audio
|
|
? {
|
|
deviceId: selecionados.microphoneId
|
|
? { exact: selecionados.microphoneId }
|
|
: undefined
|
|
}
|
|
: false,
|
|
video: video
|
|
? {
|
|
deviceId: selecionados.cameraId ? { exact: selecionados.cameraId } : undefined
|
|
}
|
|
: false
|
|
};
|
|
|
|
previewStream = await solicitarPermissaoMidia(audio, video);
|
|
if (previewStream && previewVideo) {
|
|
previewVideo.srcObject = previewStream;
|
|
}
|
|
} else {
|
|
previewVideo.srcObject = null;
|
|
}
|
|
} catch (error) {
|
|
console.error('Erro ao atualizar preview:', error);
|
|
erro = 'Erro ao acessar dispositivo de mídia.';
|
|
}
|
|
}
|
|
|
|
// Testar áudio
|
|
async function testarAudio(): Promise<void> {
|
|
if (!selecionados.microphoneId) {
|
|
erro = 'Selecione um microfone primeiro.';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const stream = await solicitarPermissaoMidia(true, false);
|
|
if (stream) {
|
|
// Criar elemento de áudio temporário para teste
|
|
const audio = new Audio();
|
|
const audioTracks = stream.getAudioTracks();
|
|
if (audioTracks.length > 0) {
|
|
// O áudio será reproduzido automaticamente se conectado
|
|
setTimeout(() => {
|
|
stream.getTracks().forEach((track) => track.stop());
|
|
}, 3000);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Erro ao testar áudio:', error);
|
|
erro = 'Erro ao testar microfone.';
|
|
}
|
|
}
|
|
|
|
function handleFechar(): void {
|
|
// Parar preview ao fechar
|
|
if (previewStream) {
|
|
previewStream.getTracks().forEach((track) => track.stop());
|
|
previewStream = null;
|
|
}
|
|
if (previewVideo) {
|
|
previewVideo.srcObject = null;
|
|
}
|
|
erro = null;
|
|
onClose();
|
|
}
|
|
|
|
function handleAplicar(): void {
|
|
onAplicar({
|
|
microphoneId: selecionados.microphoneId,
|
|
cameraId: selecionados.cameraId,
|
|
speakerId: selecionados.speakerId
|
|
});
|
|
handleFechar();
|
|
}
|
|
|
|
// Carregar dispositivos quando abrir
|
|
$effect(() => {
|
|
if (typeof window === 'undefined') return;
|
|
|
|
if (open) {
|
|
carregarDispositivos();
|
|
} else {
|
|
// Limpar preview ao fechar
|
|
if (previewStream) {
|
|
previewStream.getTracks().forEach((track) => track.stop());
|
|
previewStream = null;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Atualizar preview quando mudar seleção
|
|
$effect(() => {
|
|
if (typeof window === 'undefined') return;
|
|
|
|
if (open && (selecionados.microphoneId || selecionados.cameraId)) {
|
|
atualizarPreview();
|
|
}
|
|
});
|
|
|
|
onMount(() => {
|
|
return () => {
|
|
// Cleanup ao desmontar
|
|
if (previewStream) {
|
|
previewStream.getTracks().forEach((track) => track.stop());
|
|
}
|
|
};
|
|
});
|
|
</script>
|
|
|
|
{#if open}
|
|
<dialog
|
|
class="modal modal-open"
|
|
onclick={(e) => e.target === e.currentTarget && handleFechar()}
|
|
role="dialog"
|
|
aria-labelledby="modal-title"
|
|
>
|
|
<div class="modal-box max-w-2xl" onclick={(e) => e.stopPropagation()}>
|
|
<!-- Header -->
|
|
<div class="border-base-300 flex items-center justify-between border-b px-6 py-4">
|
|
<h2 id="modal-title" class="text-xl font-semibold">Configurações de Mídia</h2>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-circle"
|
|
onclick={handleFechar}
|
|
aria-label="Fechar"
|
|
>
|
|
<X class="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="max-h-[70vh] space-y-6 overflow-y-auto p-6">
|
|
{#if erro}
|
|
<div class="alert alert-error">
|
|
<span>{erro}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if carregando}
|
|
<div class="flex items-center justify-center py-8">
|
|
<span class="loading loading-spinner loading-lg"></span>
|
|
</div>
|
|
{:else}
|
|
<!-- Seleção de Microfone -->
|
|
<div>
|
|
<label class="text-base-content mb-2 block text-sm font-medium"> Microfone </label>
|
|
<select
|
|
class="select select-bordered w-full"
|
|
bind:value={selecionados.microphoneId}
|
|
onchange={atualizarPreview}
|
|
>
|
|
<option value={null}>Padrão do Sistema</option>
|
|
{#each dispositivos.microphones as microfone}
|
|
<option value={microfone.deviceId}>{microfone.label}</option>
|
|
{/each}
|
|
</select>
|
|
{#if selecionados.microphoneId}
|
|
<button type="button" class="btn btn-sm btn-ghost mt-2" onclick={testarAudio}>
|
|
<Volume2 class="h-4 w-4" />
|
|
Testar
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Seleção de Câmera -->
|
|
<div>
|
|
<label class="text-base-content mb-2 block text-sm font-medium"> Câmera </label>
|
|
<select
|
|
class="select select-bordered w-full"
|
|
bind:value={selecionados.cameraId}
|
|
onchange={atualizarPreview}
|
|
>
|
|
<option value={null}>Padrão do Sistema</option>
|
|
{#each dispositivos.cameras as camera}
|
|
<option value={camera.deviceId}>{camera.label}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Preview de Vídeo -->
|
|
{#if selecionados.cameraId}
|
|
<div>
|
|
<label class="text-base-content mb-2 block text-sm font-medium"> Preview </label>
|
|
<div class="bg-base-300 aspect-video w-full overflow-hidden rounded-lg">
|
|
<video
|
|
bind:this={previewVideo}
|
|
autoplay
|
|
muted
|
|
playsinline
|
|
class="h-full w-full object-cover"
|
|
></video>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Seleção de Alto-falante (se disponível) -->
|
|
{#if dispositivos.speakers.length > 0}
|
|
<div>
|
|
<label class="text-base-content mb-2 block text-sm font-medium"> Alto-falante </label>
|
|
<select class="select select-bordered w-full" bind:value={selecionados.speakerId}>
|
|
<option value={null}>Padrão do Sistema</option>
|
|
{#each dispositivos.speakers as speaker}
|
|
<option value={speaker.deviceId}>{speaker.label}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="modal-action border-base-300 border-t px-6 py-4">
|
|
<button type="button" class="btn btn-ghost" onclick={handleFechar}> Cancelar </button>
|
|
<button type="button" class="btn btn-primary" onclick={handleAplicar} disabled={carregando}>
|
|
<Check class="h-4 w-4" />
|
|
Aplicar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<form method="dialog" class="modal-backdrop">
|
|
<button type="button" onclick={handleFechar}>fechar</button>
|
|
</form>
|
|
</dialog>
|
|
{/if}
|