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

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

영상 오디오 WebRTC loopback + EC 스텝 토글 제거 — iO…: 입력: 1. 왜 이 구조가 됐나 — 인과 체인, 주요 처리 단계: 2. 적용 1 — 영상 오디오 WebRTC loopback 라우팅, 결과: 6. 출력 경로별 덕킹 매트릭스 (EC 상시 ON 이후) 흐름
동작 흐름 요약
  1. 입력: 1. 왜 이 구조가 됐나 — 인과 체인
  2. 주요 처리 단계: 2. 적용 1 — 영상 오디오 WebRTC loopback 라우팅
  3. 결과: 6. 출력 경로별 덕킹 매트릭스 (EC 상시 ON 이후)
💬 대화로 먼저 이해하기 — "통화 중 볼륨 자동 낮춤 비유" (비개발자·처음 읽는 사람용)
QiOS에서 영상 소리가 갑자기 작아지던 문제가 뭐였나요?
A운전 중 전화가 오면 차가 라디오 볼륨을 알아서 줄이죠. iOS도 마이크가 통화 모드로 켜져 있으면 영상·TTS 같은 "미디어" 소리를 자동으로 줄여요(덕킹). 세션은 마이크를 계속 쓰니 영상 소리가 늘 작아졌던 거예요.
Q그걸 어떻게 피했어요?
AiOS는 "통화 상대의 목소리"는 줄이지 않아요. 그래서 영상 소리를 기기 안에서 자기 자신에게 전화를 거는 회선(WebRTC loopback)에 실어 보내, OS가 통화 상대 음성으로 분류하게 만들었어요. 라디오 소리를 전화 회선에 태워 보내는 셈이라 볼륨이 안 줄어들죠.
Q예전에는 스텝마다 마이크 설정(EC)을 껐다 켰다 했다던데요?
A그 토글이 에코 전사·clone 무음 같은 부작용의 근원이어서 이번에 완전히 제거했어요. 이제 EC/NS/AGC는 세션 내내 ON 고정이고, 덕킹 회피는 출력 경로(loopback)가 전담해요.
Q새 방식의 부작용은 없나요?
Aloopback 오디오가 빨라졌다 느려졌다 하는 출렁임이 있었는데, 수신 버퍼 고정(jitterBufferTarget 150ms) 등으로 억제했어요(섹션 4). 문제 발생 시 조정 손잡이는 섹션 7의 런타임 튜너블(videoLoopbackGain·videoLoopbackJbt)을 보세요.
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