216 lines
7.9 KiB
TypeScript

import React, { useEffect, useRef, useState } from 'react'
import styles from './PreJoin.module.css'
import { ControlBar, ControlButton, MicrophoneMeter, modifierKeyLabel, isMacPlatform } from 'avanza-ui'
import { FiMic, FiVideo, FiSettings } from 'react-icons/fi'
type Props = {
roomName?: string
onProceed: () => void
onCancel?: () => void
serverUrl?: string
token?: string
}
export default function PreJoin({ roomName, onProceed, onCancel }: Props) {
const videoRef = useRef<HTMLVideoElement | null>(null)
const [name, setName] = useState(() => {
try { return localStorage.getItem('avanzacast_user') || '' } catch { return '' }
})
const [micEnabled, setMicEnabled] = useState(true)
const [camEnabled, setCamEnabled] = useState(true)
const [error, setError] = useState<string | null>(null)
const [isChecking, setIsChecking] = useState(false)
// checkbox state is local only; do NOT persist skip preference so PreJoin always appears
const [skipNextTime, setSkipNextTime] = useState<boolean>(false)
// keep preview stream active for meter and preview
const [previewStream, setPreviewStream] = useState<MediaStream | null>(null)
// Use shared platform utils
const isMac = isMacPlatform()
const modLabel = modifierKeyLabel()
const micHint = `${modLabel.display} + D`
const camHint = `${modLabel.display} + E`
useEffect(() => {
// ensure any old skip flag does not affect behavior: remove legacy key
try { localStorage.removeItem('broadcast:skipPrejoin') } catch (e) {}
// request preview stream whenever toggles change
let mounted = true
let localStream: MediaStream | null = null
;(async () => {
try {
if (!navigator?.mediaDevices?.getUserMedia) return
localStream = await navigator.mediaDevices.getUserMedia({ audio: micEnabled, video: camEnabled })
if (!mounted) {
try { localStream.getTracks().forEach(t => t.stop()) } catch (e) {}
return
}
setPreviewStream(localStream)
if (videoRef.current) {
videoRef.current.srcObject = localStream
videoRef.current.play().catch(() => {})
}
} catch (e: any) {
// ignore permission errors
}
})()
// Keyboard shortcuts: toggle mic/camera. Support Ctrl on Windows/Linux and Meta (⌘) on macOS.
const onKeyDown = (e: KeyboardEvent) => {
// ignore when focused on input/textarea or when modifier keys conflict with browser shortcuts
const active = document.activeElement;
const tag = active && (active as HTMLElement).tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA' || (active as HTMLElement)?.isContentEditable) return;
const mod = isMac ? e.metaKey : e.ctrlKey
// Mod + D -> toggle mic
if (mod && !e.shiftKey && (e.key === 'd' || e.key === 'D')) {
e.preventDefault()
setMicEnabled(v => !v)
}
// Mod + E -> toggle camera (requested)
if (mod && !e.shiftKey && (e.key === 'e' || e.key === 'E')) {
e.preventDefault()
setCamEnabled(v => !v)
}
// Mod + Shift + C -> alternate camera shortcut (also supported)
if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'c' || e.key === 'C')) {
e.preventDefault()
setCamEnabled(v => !v)
}
}
window.addEventListener('keydown', onKeyDown)
return () => {
mounted = false
if (localStream) {
try { localStream.getTracks().forEach(t => t.stop()) } catch (e) {}
}
// clear previewStream state
setPreviewStream(null)
window.removeEventListener('keydown', onKeyDown)
}
}, [micEnabled, camEnabled])
const handleProceed = async () => {
setError(null)
setIsChecking(true)
try {
// request permissions explicitly
await navigator.mediaDevices.getUserMedia({ audio: micEnabled, video: camEnabled })
// save name
try { if (name) localStorage.setItem('avanzacast_user', name) } catch (e) {}
// proceed to connect
onProceed()
} catch (e: any) {
setError(e?.message || 'No se pudo acceder a la cámara/micrófono')
} finally {
setIsChecking(false)
}
}
const toggleMic = async () => {
setMicEnabled(v => !v)
}
const toggleCam = async () => {
setCamEnabled(v => !v)
}
return (
<div className={styles.prejoinContainer}>
<div className={styles.card}>
<div className={styles.header}>
<div>Configura tu estudio</div>
<div className={styles.note}>Entrar al estudio no iniciará automáticamente la transmisión.</div>
</div>
<div className={styles.contentRow}>
<div className={styles.previewColumn}>
<div className={styles.previewCard}>
<video ref={videoRef} className={styles.videoEl} playsInline muted />
<div className={styles.badge}>{name || 'Invitado'}</div>
<div className={styles.micPanel}>
<div className={styles.micPanelInner}>
<MicrophoneMeter level={previewStream ? 1 : 0} />
<div className={styles.micStatus}>{micEnabled ? 'El micrófono está funcionando' : 'Micrófono desactivado'}</div>
</div>
</div>
</div>
<div className={styles.controlsRow}>
<ControlButton
className={styles.controlButtonLocal}
icon={<FiMic />}
label={micEnabled ? 'Desactivar audio' : 'Activar audio'}
active={micEnabled}
danger={!micEnabled}
layout="column"
variant="studio"
onClick={toggleMic}
hint={micHint}
size="md"
/>
<ControlButton
className={styles.controlButtonLocal}
icon={<FiVideo />}
label={camEnabled ? 'Detener cámara' : 'Iniciar cámara'}
active={camEnabled}
danger={!camEnabled}
layout="column"
variant="studio"
onClick={toggleCam}
hint={camHint}
size="md"
/>
<ControlButton
className={styles.controlButtonLocal}
icon={<FiSettings />}
label={'Configuración'}
active={true}
layout="column"
variant="studio"
onClick={() => { /* abrir modal de settings si aplica */ }}
size="md"
/>
</div>
{/* Leyenda de atajos: muestra las combinaciones detectadas (ej: ⌘ + D) */}
<div className={styles.shortcutsLegend} aria-hidden="true">
Atajos: <span className={styles.kbd}>{micHint}</span> mic · <span className={styles.kbd}>{camHint}</span> cámara
</div>
</div>
<div className={styles.sideColumn}>
{error && <div className={styles.error}>{error}</div>}
<div className={styles.roomTitle}>Nombre para mostrar</div>
<input className={styles.input} value={name} onChange={e => setName(e.target.value)} placeholder="Tu nombre" />
<div className={styles.roomTitle}>Título (opcional)</div>
<input className={styles.input} placeholder="p. ej.: Founder of Creativity Inc" />
<div className={styles.checkboxRow}>
<input id="skipNext" type="checkbox" checked={skipNextTime} onChange={e => setSkipNextTime(e.target.checked)} />
<label htmlFor="skipNext">Omitir PreJoin la próxima vez</label>
</div>
<div className={styles.actions}>
<button className={styles.cancelBtn} onClick={() => { onCancel?.() }}>Cancelar</button>
<button className={styles.primaryBtn} onClick={handleProceed} disabled={isChecking}>{isChecking ? 'Comprobando...' : 'Entrar al estudio'}</button>
</div>
</div>
</div>
</div>
</div>
)
}