영상 오디오 WebRTC loopback + EC 스텝 토글 제거 — iOS 덕킹 회피 (PPI-1121)

마지막 업데이트 2026-07-22

iOS 게스트에서 영상(full:video) 오디오를 WebRTC loopback(통화 카테고리)으로 로컬 출력해 EC(voice-processing) 상시 ON 상태에서도 미디어 덕킹을 회피하고, 사이드 이펙트의 근원이던 PPI-1097 EC/NS/AGC 스텝 토글을 완전히 제거한 구현 문서. 2026-07-13 iPad 실기기 작동 확인 완료 (재생 속도 출렁임 억제 포함).
티켓PPI-1121 (계보: PPI-1097 영상 감쇠 → PPI-1101 TTS 덕킹 → PPI-1121 clone 무음/에코)
커밋1f3c3e67 loopback 라우팅 + EC 토글 제거 · 05fd985e 재생 속도 출렁임 억제 (브랜치 PPI-1121)
적용 범위iOS 계열(iPhone/iPad, iPadOS 13+ 데스크톱 UA 위장 포함) 게스트 세션에서만. PC/Android 게스트·모니터 페이지는 기존 video 직접 재생 유지
검증 상태iPad 실기기 확인 덕킹 회피 + 출렁임 억제 작동 (2026-07-13)
핵심 파일shared/lib/webrtc-audio-loopback.ts · shared/lib/video-audio-loopback.ts · shared/ui/meet-video.tsx · shared/ui/guest-layout-content.tsx

1. 왜 이 구조가 됐나 — 인과 체인

덕킹의 원인 체인 (iOS 관찰 동작):

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 재생  ←★ 통화 분류 = 덕킹 면제

게이팅 — 한 곳으로 수렴

폴백 정책 (무음 구간 0 원칙)

3. 적용 2 — EC 스텝 토글 제거

이전 (PPI-1097 + 1dc7317d iOS 게이트)이후 (이번 적용)
영상/이미지 스텝EC/NS/AGC OFF (덕킹 조건 이탈)EC ON 유지, 영상 오디오는 loopback으로 덕킹 면제
AI 스텝 복귀applyConstraints로 ON 복원 (실패 시 고착)복원 동작 자체가 없음
voice-processing스텝마다 재구성 (clone 무음 유발 조건)세션 내내 안정 (재구성 0회)
제거 코드use-guest-page-session.ts의 스텝 토글 useEffect, local-media-constraints.tsapplyMicProcessingConstraints (마지막 런타임 EC 조작 코드)
기대 효과: 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=미지원)
Opus 균일화패킷 크기/간격 일정화로 NetEQ 적응 유발 조건 축소answer SDP fmtp 패치: cbr=1;usedtx=0;maxaveragebitrate=256000;stereo=1 (stabilizeOpusSdp)
contentHint연속 신호용 인코딩 모드송신 트랙 contentHint="music"

세 옵션 모두 VideoAudioLoopback.ensureLoopback() 안에서만 전달되므로 loopback 적용 조건(iOS 게스트)을 그대로 상속한다. TTS loopback은 옵션 미전달로 기존 검증 동작 유지. 네트워크 속도와는 무관한 경로(기기 내부 host candidate + 사전 캐시 blob)지만, 극한 네트워크에서 다른 연결(SFU/Realtime)의 복구 처리가 CPU를 눌러 간접 재발할 이론적 여지는 있음 → 그 경우 videoLoopbackJbt 상향으로 대응.

5. 파일 역할 분리

TtsPlayer (entities/guest-session/lib/tts-player.ts) ──┐
                                                       ├──▶ createWebRtcAudioLoopback()   ← "어떻게 되받나" (공용 전송 계층)
VideoAudioLoopback (shared/lib/video-audio-loopback.ts)┘        shared/lib/webrtc-audio-loopback.ts
        ▲
MeetVideo (iOS 게스트 세션에서만 컨트롤러 생성)                  ← "무엇을, 실패하면 어떻게" (소비자별 정책 계층)
파일역할
webrtc-audio-loopback.ts무상태 공용 유틸. 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.tsTTS 전용 정책(기존): 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 > 기본값)

기본값용도
videoLoopbackGain0.1 (허용 0~4)loopback 로컬 출력 감쇠 — TTS 캘리브레이션 기준 차용, 실기기 미세조정용
videoLoopbackJbt150ms (허용 0~1000, 0=미적용)수신 버퍼 고정 목표 — 출렁임 잔존 시 상향, 립싱크 이슈 시 하향
videoLoopback=offloopback 비활성(직결만) — 출렁임/음질 문제의 loopback 기인 여부 A/B 확진용

로그 시그니처 (prefix VIDEO_AUDIO_LOOPBACK)

로그의미
Video audio routed into loopback graphattach 성공 (localGain·loopbackState 포함)
Video audio loopback establishedloopback 성립 + remote 재생 시작 (attempts·localGain·jitterBufferTargetMs·jitterBufferApplied)
... setup failed / ... playback failed, using direct output협상 실패(재시도) / play 거부(직결 확정 폴백)
... connection lost성립 후 연결 사망 → 직결 복귀 + 재협상
... disabled by overridevideoLoopback=off A/B 모드

8. 검증 상태 · 남은 항목

9. 관련 문서

작성: 2026-07-13 / chulsu · 커밋 1f3c3e67, 05fd985e (브랜치 PPI-1121) · iPad 실기기 검증 2026-07-13