getUserMedia(EC on) → WebKit이 voice-processing I/O 유닛으로 캡처
→ 오디오 세션이 통화 모드(playAndRecord/voiceChat 계열)로 전환
→ OS가 통화 명료도를 위해 "미디어 카테고리" 출력을 자동 감쇠(덕킹)
→ HTMLMediaElement 재생(영상 src, TTS blob)이 감쇠됨
단, WebRTC remote 트랙(srcObject) 재생은 "통화 상대 음성"으로 분류 → 덕킹 면제 ←★ 회피 원리
기존 대응(PPI-1097 EC 스텝 토글)의 사이드 이펙트 체인:
비-AI 스텝 진입 시 applyConstraints로 EC/NS/AGC OFF → AI 스텝 복귀 시 ON 복원
→ iOS에서 EC 토글 = voice-processing 유닛 전체 재구성
→ ① 복원 실패 시 EC OFF "고착" → 스피커 TTS가 마이크로 재유입 → 에코 전사(확정 시그니처)
→ ② 재구성 중 producer clone이 결정론적 무음 (PPI-1121 clone 무음 계열)
→ ③ OpenAI에 에코+실음성 혼입 → VAD 혼란 → "아동 말이 전달 안 됨" 체감
결론: 입력(마이크 constraint)은 절대 건드리지 않고, 출력을 전부 덕킹 면제 분류(통화 채널)로 옮긴다.
TTS는 PPI-1101에서 이미 loopback으로 옮겼고, 이번에 영상까지 옮기면서 EC 토글의 존재 이유가 사라져 제거했다.
이제 EC/NS/AGC는 세션 내내 ON 고정이다 (런타임에 끄는 코드가 코드베이스에 더 이상 없음).
2. 적용 1 — 영상 오디오 WebRTC loopback 라우팅
<video src=blob> ──(MediaElementAudioSourceNode, element당 1회·WeakMap 캐시)──▶ hub(GainNode)
│
┌─────────────────────────────────────────────────────────┤
▼ ▼
localGain(0.1) → MediaStreamAudioDestination ctx.destination 직결
│ (loopback feed) (loopback 미성립 시에만 연결,
▼ 원음 유지 — 잔존 덕킹 감수)
RTCPeerConnection 쌍 (sender→receiver, 기기 내부 host candidate)
▼
remote 트랙 → 숨김 audio.srcObject 재생 ←★ 통화 분류 = 덕킹 면제
게이팅 — 한 곳으로 수렴
guest-layout-content.tsx: isSessionProp && isIosFamilyDevice일 때만
세션의 getSharedAudioContext(사용자 제스처 시점 생성/resume된 공유 ctx getter)를 MeetVideo에 주입
meet-video.tsx: prop이 없으면 VideoAudioLoopback 컨트롤러를 아예 생성하지 않음(no-op) —
모니터/PC/Android는 코드 경로 자체가 비활성
attach 시점: videoUrl 변경 effect + play 이벤트 (멱등, ctx 미존재 시 다음 호출에서 재시도)
폴백 정책 (무음 구간 0 원칙)
협상 대기/실패/연결 사망 시 hub → ctx.destination 직결로 소리 유지 (잔존 덕킹 감수, TTS blob 폴백과 같은 이유로 감쇠 없음)
협상 실패는 3회 상한 내 재시도, audio.play() 거부(자동재생 정책)는 직결로 확정 폴백
직결↔loopback 동시 출력은 수십 ms 지연차 slap echo가 되므로 직결을 먼저 끊고 remote 재생 시작 (전환 갭이 에코보다 낫다)
createMediaElementSource는 element당 1회 제약(재호출 시 InvalidStateError) → 모듈 레벨 WeakMap 캐시로 StrictMode 이중 마운트 방어
기대 효과: EC OFF 고착 → 에코 전사, voice-processing 재구성 → clone 무음의 발생 조건이 원천 제거된다.
확정 실험: 같은 기기에서 에코 전사·clone 무음이 동반 소멸하는지 세션 로그로 확인.
부수 변화: 영상 스텝 중 EC가 살아 있어 아동 마이크로 새던 영상 소리가 이제 상당 부분 제거됨(모니터는 자체 재생이라 무영향).
4. 적용 3 — 재생 속도 출렁임 억제 (loopback 내부 옵션)
증상: iPad 실기기에서 loopback 영상 오디오가 빨라졌다 느려졌다 함.
원인: WebRTC 수신측 적응형 지터 버퍼(NetEQ)가 버퍼 수위를 맞추려 오디오를 시간 압축/확장하는 표준 동작.
로컬 loopback이라 네트워크 지터는 없지만, iPad 부하(영상 디코드+SFU+Realtime 동시 구동)로 송신 패킷 생산이
불규칙하면 같은 증상이 난다. 말소리(TTS)에선 안 들리고 연속 오디오(영상)에서 드러난다.
대응
메커니즘
구현
수신 버퍼 고정
receiver.jitterBufferTarget = 150ms — 적응 대신 고정 목표로 재생. 대가는 목표값만큼의 고정 오디오 지연(립싱크)
미지원 시 playoutDelayHint 폴백, 적용 API를 established 로그의 jitterBufferApplied에 기록 (null=미지원)
세 옵션 모두 VideoAudioLoopback.ensureLoopback() 안에서만 전달되므로
loopback 적용 조건(iOS 게스트)을 그대로 상속한다. TTS loopback은 옵션 미전달로 기존 검증 동작 유지.
네트워크 속도와는 무관한 경로(기기 내부 host candidate + 사전 캐시 blob)지만,
극한 네트워크에서 다른 연결(SFU/Realtime)의 복구 처리가 CPU를 눌러 간접 재발할 이론적 여지는 있음 → 그 경우 videoLoopbackJbt 상향으로 대응.
무상태 공용 유틸. PC 쌍 협상, ICE candidate 지연 전달(조기 도착 유실 방지), remote track 3초 타임아웃(Promise.race), 연결 사망 1회 통지(onConnectionLost), Opus SDP 패치·jitterBufferTarget·contentHint 옵션
video-audio-loopback.ts
영상 전용 정책. WebAudio 그래프(hub/게인/직결), element 캐시, 폴백·재시도 상태기계, 런타임 튜너블, 숨김 audio element 관리
tts-player.ts
TTS 전용 정책(기존): blob 폴백, chunk 진행, 로컬 감쇠 게인 0.1. 이번에 협상 로직만 공용 유틸로 위임
6. 출력 경로별 덕킹 매트릭스 (EC 상시 ON 이후)
출력
경로
덕킹
감쇠 게인
TTS (정상)
loopback → srcObject
면제
LOOPBACK_LOCAL_GAIN 0.1 (실기기 확정)
TTS (폴백)
blob URL 재생
맞음 (감수)
없음 (원음으로 보상)
영상 (정상)
loopback → srcObject
면제
VIDEO_LOOPBACK_LOCAL_GAIN 0.1 (TTS 기준 차용)
영상 (폴백)
WebAudio ctx.destination 직결
잔존 (감수)
없음 (원음)
이미지 스텝
오디오 없음
—
—
7. 런타임 튜너블 · 로그 시그니처
튜너블 (URL 쿼리 > localStorage > 기본값)
키
기본값
용도
videoLoopbackGain
0.1 (허용 0~4)
loopback 로컬 출력 감쇠 — TTS 캘리브레이션 기준 차용, 실기기 미세조정용
videoLoopbackJbt
150ms (허용 0~1000, 0=미적용)
수신 버퍼 고정 목표 — 출렁임 잔존 시 상향, 립싱크 이슈 시 하향
videoLoopback=off
—
loopback 비활성(직결만) — 출렁임/음질 문제의 loopback 기인 여부 A/B 확진용
로그 시그니처 (prefix VIDEO_AUDIO_LOOPBACK)
로그
의미
Video audio routed into loopback graph
attach 성공 (localGain·loopbackState 포함)
Video audio loopback established
loopback 성립 + remote 재생 시작 (attempts·localGain·jitterBufferTargetMs·jitterBufferApplied)
... setup failed / ... playback failed, using direct output
협상 실패(재시도) / play 거부(직결 확정 폴백)
... connection lost
성립 후 연결 사망 → 직결 복귀 + 재협상
... disabled by override
videoLoopback=off A/B 모드
8. 검증 상태 · 남은 항목
완료 iPad 실기기: 덕킹 회피 + 출렁임 억제 작동 확인 (2026-07-13, dev 배포)