Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
type SourceState,
type UpdateState
} from '@shared/types'
import { channelsFor, MeetingRecorder, type Levels, type Source } from './audio/recorder'
import { channelsFor, MeetingRecorder, micLabel, type Levels, type Source } from './audio/recorder'
import { MeetingView } from './components/MeetingView'
import { Onboarding } from './components/Onboarding'
import { ScreenPrompt, type ScreenChoice } from './components/ScreenPrompt'
Expand Down Expand Up @@ -360,6 +360,7 @@ function Shell(): React.JSX.Element {
mic: captureMic,
system: captureSystem,
micDeviceId: settings.micDeviceId,
micDeviceLabel: settings.micDeviceLabel,
separate: settings.separateMic,
screen: withScreen
}
Expand All @@ -375,6 +376,12 @@ function Shell(): React.JSX.Element {
throw e
}
recorder.current = rec
rec.warnings.forEach((w) => ui.toast(w, 'info'))
// Encontrado por nombre con otro identificador, o ya no existe (se usa el predeterminado):
// se guarda lo que se ha usado para no repetir la búsqueda ni el aviso.
if (captureMic && rec.micDeviceId !== settings.micDeviceId) {
void saveSettings({ ...settings, micDeviceId: rec.micDeviceId })
}
setScreenOn(withScreen)
setLiveNotice(null)
setPaused(false)
Expand Down Expand Up @@ -722,7 +729,9 @@ function Shell(): React.JSX.Element {
captureSystem={captureSystem}
onCaptureMic={setCaptureMic}
onCaptureSystem={setCaptureSystem}
onMicDevice={(micDeviceId) => void saveSettings({ ...settings, micDeviceId })}
onMicDevice={(micDeviceId) =>
void micLabel(micDeviceId).then((micDeviceLabel) => saveSettings({ ...settings, micDeviceId, micDeviceLabel }))
}
onScreen={(patch) => void saveSettings({ ...settings, ...patch })}
screenOn={screenOn}
onToggleScreen={() => void toggleScreen()}
Expand Down
34 changes: 33 additions & 1 deletion src/renderer/src/audio/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export interface CapturePlan {
mic: boolean
system: boolean
micDeviceId: string
/** Nombre del micrófono elegido: sirve para encontrarlo si su identificador cambia. */
micDeviceLabel?: string
/** Transcribir el micrófono aparte (siempre "yo") y el sistema aparte. */
separate: boolean
/** Grabar también la pantalla elegida (screen.mp4, con el audio mezclado). */
Expand Down Expand Up @@ -46,6 +48,10 @@ export class MeetingRecorder {
private meetingId = ''
private mixStream: MediaStream | null = null
private screenTrack: MediaStreamTrack | null = null
/** Avisos que no impiden grabar (p. ej. micrófono elegido no disponible). */
warnings: string[] = []
/** Micrófono que se está usando de verdad. */
micDeviceId = ''

constructor(private onLevels?: (l: Levels) => void) {}

Expand Down Expand Up @@ -78,9 +84,12 @@ export class MeetingRecorder {
}
}
if (p.mic) {
const mic = await resolveMic(p.micDeviceId, p.micDeviceLabel ?? '')
if (mic.warning) this.warnings.push(mic.warning)
this.micDeviceId = mic.deviceId ?? ''
micStream = await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: p.micDeviceId ? { exact: p.micDeviceId } : undefined,
deviceId: mic.deviceId ? { exact: mic.deviceId } : undefined,
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
Expand Down Expand Up @@ -264,3 +273,26 @@ export class MeetingRecorder {
return duration
}
}

/**
* Encuentra el micrófono elegido. Su identificador depende de desde dónde se abra la
* app (instalada o en desarrollo) y puede cambiar al reinstalar drivers, así que si no
* aparece se busca por nombre; si tampoco, se usa el predeterminado y se avisa.
*/
export async function resolveMic(id: string, label: string): Promise<{ deviceId?: string; warning?: string }> {
if (!id) return {}
const inputs = (await navigator.mediaDevices.enumerateDevices()).filter((d) => d.kind === 'audioinput')
if (inputs.some((d) => d.deviceId === id)) return { deviceId: id }
const byLabel = label ? inputs.find((d) => d.label === label) : undefined
if (byLabel) return { deviceId: byLabel.deviceId }
return {
warning: `No se encuentra el micrófono elegido${label ? ` (${label})` : ''}: se usa el predeterminado del sistema. Puedes cambiarlo en la flecha junto a Micrófono.`
}
}

/** Nombre de un micrófono por su identificador (para guardarlo junto a él). */
export async function micLabel(id: string): Promise<string> {
if (!id) return ''
const devices = await navigator.mediaDevices.enumerateDevices()
return devices.find((d) => d.kind === 'audioinput' && d.deviceId === id)?.label ?? ''
}
6 changes: 5 additions & 1 deletion src/renderer/src/components/Onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { ApiKeys, FinalProvider, KeySource, LiveProvider, LlmProvider, Loca
import { finalProviderFor, languageNames, LANGUAGES, liveProviderFor } from '@shared/languages'
import { Field, Logo, quick, soft, spring, Toggle } from './ui'
import { MultiSelect } from './Select'
import { micLabel } from '../audio/recorder'
import { keyInfo, KeyInput, KeyStatus, useKeyChecks, type KeyState } from './keys'
import { languageHint, MicField, SEPARATE_MIC_DESC } from './SettingsView'
import { LocalAiCard, localAiSummary, useLocalAi } from './LocalAi'
Expand Down Expand Up @@ -370,7 +371,10 @@ export function Onboarding({ settings, closable, onSave, onFinish, onClose }: Pr
<h2>
<Mic size={16} /> Tu micrófono
</h2>
<MicField value={d.micDeviceId} onChange={(micDeviceId) => set({ micDeviceId })} />
<MicField
value={d.micDeviceId}
onChange={(micDeviceId) => void micLabel(micDeviceId).then((micDeviceLabel) => set({ micDeviceId, micDeviceLabel }))}
/>
<p className="muted small">Pulsa Probar micrófono y habla: la barra debería moverse con tu voz.</p>
</section>
<section className="card">
Expand Down
8 changes: 4 additions & 4 deletions src/renderer/src/components/Player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,12 @@ export const Player = forwardRef<PlayerHandle, Props>(function Player(
}

const toggleFullscreen = (): void => {
if (document.fullscreenElement) void document.exitFullscreen()
if (document.fullscreenElement) void document.exitFullscreen().catch(() => {})
else if (withVideo) void stage.current?.requestFullscreen()
}

const togglePip = (): void => {
if (document.pictureInPictureElement) void document.exitPictureInPicture()
if (document.pictureInPictureElement) void document.exitPictureInPicture().catch(() => {})
else void screen.current?.requestPictureInPicture().catch(() => {})
}

Expand Down Expand Up @@ -264,8 +264,8 @@ export const Player = forwardRef<PlayerHandle, Props>(function Player(
// Si se esconde el vídeo, fuera de pantalla completa y de PiP.
useEffect(() => {
if (withVideo) return
if (document.fullscreenElement === stage.current) void document.exitFullscreen()
if (document.pictureInPictureElement === screen.current) void document.exitPictureInPicture()
if (document.fullscreenElement === stage.current) void document.exitFullscreen().catch(() => {})
if (document.pictureInPictureElement === screen.current) void document.exitPictureInPicture().catch(() => {})
}, [withVideo])

const readBuffered = (): void => {
Expand Down
6 changes: 5 additions & 1 deletion src/renderer/src/components/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { CostEstimate } from './CostEstimate'
import { GlossarySettings } from './GlossarySettings'
import { KEYS, KeyInput, KeyStatus, useKeyChecks, type KeyInfo } from './keys'
import { ComboInput, MultiSelect, Select } from './Select'
import { micLabel } from '../audio/recorder'
import { knownModels, llmPrice } from '@shared/pricing'
import { finalProviderFor, languageNames, LANGUAGES, liveProviderFor, needsElevenLabs } from '@shared/languages'

Expand Down Expand Up @@ -753,7 +754,10 @@ function AudioSettings({ s, set }: { s: Settings; set: (p: Partial<Settings>) =>
<>
<section className="card">
<h2>Micrófono</h2>
<MicField value={s.micDeviceId} onChange={(micDeviceId) => set({ micDeviceId })} />
<MicField
value={s.micDeviceId}
onChange={(micDeviceId) => void micLabel(micDeviceId).then((micDeviceLabel) => set({ micDeviceId, micDeviceLabel }))}
/>
</section>
<section className="card">
<h2>Separación de tu voz</h2>
Expand Down
16 changes: 12 additions & 4 deletions src/renderer/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,19 @@ export function initials(label: string): string {
return (parts[0][0] + (parts[1]?.[0] ?? '')).toUpperCase()
}

const CAPTURE_ERRORS: Record<string, string> = {
OverconstrainedError: 'El micrófono elegido no está disponible. Elige otro en la flecha junto a Micrófono.',
NotFoundError: 'No se ha encontrado ningún micrófono. Conecta uno o desactiva Micrófono.',
NotReadableError: 'No se puede usar el micrófono: puede que otra aplicación lo esté usando en exclusiva.',
NotAllowedError: 'Windows no ha dado permiso para capturar el audio. Revisa la privacidad del micrófono en Configuración de Windows.',
AbortError: 'Se ha interrumpido la captura de audio. Vuelve a intentarlo.'
}

export function errorMessage(e: unknown): string {
return String((e as Error)?.message ?? e).replace(
/^Error invoking remote method '[^']+': (Error: )?/,
''
)
const err = e as Error | undefined
const text = String(err?.message ?? e ?? '').replace(/^Error invoking remote method '[^']+': (Error: )?/, '').trim()
if (err?.name && CAPTURE_ERRORS[err.name]) return text ? `${CAPTURE_ERRORS[err.name]} (${text})` : CAPTURE_ERRORS[err.name]
return text || `Ha ocurrido un error inesperado${err?.name && err.name !== 'Error' ? ` (${err.name})` : ''}.`
}

/** Minúsculas y sin tildes, carácter a carácter (mantiene la correspondencia de posiciones). */
Expand Down
2 changes: 2 additions & 0 deletions src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ export interface Settings {
/** Idiomas que se hablan en las reuniones. Vacío = detectar cualquiera. */
languages: string[]
micDeviceId: string
/** Nombre del micrófono elegido, para reconocerlo si cambia su identificador. */
micDeviceLabel?: string
/** Transcribe el micrófono aparte y lo etiqueta siempre como "yo". */
separateMic: boolean
/** Grabar también la pantalla y cuál (identificador del monitor). */
Expand Down
Loading