// src/renderer/utils/systemAudioCapture.ts // 시스템 오디오(데스크톱 소리) 캡처 — Electron setDisplayMediaRequestHandler + audio: 'loopback' // contextIsolation: true 환경에서 preload IPC 브릿지를 통해 동작 let mediaStream: MediaStream | null = null let audioContext: AudioContext | null = null let processorNode: ScriptProcessorNode | null = null const TARGET_SAMPLE_RATE = 16000 /** * 시스템 오디오 캡처를 시작한다. * 1. preload를 통해 메인 프로세스에 loopback 핸들러 등록 요청 * 2. getDisplayMedia로 시스템 오디오 MediaStream 획득 * 3. ScriptProcessorNode로 PCM16 16kHz mono 변환 후 콜백 전달 */ export async function startSystemAudioCapture( onAudioData: (pcm16Buffer: ArrayBuffer) => void, ): Promise { if (mediaStream) { throw new Error('System audio capture already active') } // 1. 메인 프로세스에 loopback 핸들러 등록 await window.electronAPI.caption.enableLoopback() // 2. getDisplayMedia — video: true 필수 (Chromium 제약), 이후 video track 제거 try { mediaStream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true, }) } catch (err) { // loopback 핸들러 해제 await window.electronAPI.caption.disableLoopback() throw err } // 3. loopback 핸들러 해제 (다른 getDisplayMedia 호출에 영향 방지) await window.electronAPI.caption.disableLoopback() // 4. 비디오 트랙 제거 mediaStream.getVideoTracks().forEach((t) => { t.stop() mediaStream?.removeTrack(t) }) const audioTrack = mediaStream.getAudioTracks()[0] if (!audioTrack) { stopSystemAudioCapture() throw new Error('No audio track in loopback stream') } // 5. AudioContext로 PCM 추출 audioContext = new AudioContext({ sampleRate: TARGET_SAMPLE_RATE }) const source = audioContext.createMediaStreamSource(new MediaStream([audioTrack])) const bufferSize = 4096 processorNode = audioContext.createScriptProcessor(bufferSize, 1, 1) processorNode.onaudioprocess = (event: AudioProcessingEvent) => { const inputData = event.inputBuffer.getChannelData(0) // Float32 → PCM16 변환 const pcm16 = new Int16Array(inputData.length) for (let i = 0; i < inputData.length; i++) { const s = Math.max(-1, Math.min(1, inputData[i])) pcm16[i] = s < 0 ? s * 0x8000 : s * 0x7fff } onAudioData(pcm16.buffer) } source.connect(processorNode) processorNode.connect(audioContext.destination) } export function stopSystemAudioCapture(): void { if (processorNode) { processorNode.disconnect() processorNode.onaudioprocess = null processorNode = null } if (audioContext) { audioContext.close().catch(() => { /* ignore */ }) audioContext = null } if (mediaStream) { mediaStream.getTracks().forEach((t) => t.stop()) mediaStream = null } } export function isSystemAudioCaptureActive(): boolean { return mediaStream !== null }