iOS WebKit clone track 무음 — 현재 상황 · 원인 · 근본 해결방안 (PPI-1121)

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

iOS(iPad) WebKit에서 MediaStreamTrack.clone()으로 만든 mic 트랙이 확률적으로 live 상태인데 무음(zero-audio)이 되어, AI 대화·전사는 정상인데 진행자(모니터)에게만 아동 목소리가 전달되지 않는 문제의 종합 문서. ① 현재 상황 ② 무음 발생 원인(2026-07-09 박서준 로그 대조 확진 + WebKit 공개 이슈 대조) ③ 근본 해결방안(적용된 자가회복 가드 + clone 축소 실험 + clone 제거)을 정리한다.

1. 현재 상황

증상아동 mic-audio가 진행자 측에 전달 안 됨. AI(핑퐁이) 대화·전사는 정상 — 이 비대칭이 시그니처
발생 조건iOS/iPadOS WebKit 게스트에서 mic producer용 clone 트랙 생성 시(수업 입장·AI 세션 시작·활동 전환마다) 확률적으로 발생. 관측 사례는 전부 iPad — 7/6 홍서진(CriOS, iOS는 모든 브라우저가 WebKit 엔진 강제), 7/9 박서준(iPadOS 18.7.9)
현장 영향진행자가 모니터 새로고침으로는 복구 불가 → 강제 kick(아동 재입장)만 회복. 박서준 세션에서 새로고침 2회 + kick 3회로 수업 흐름 반복 중단
진단 수단게스트 송신측 [MEDIASOUP_PRODUCER] Audio producer outbound stats + 진행자 수신측 [MONITOR_CONSUMER] energy 측정 (7/8 로깅 범위 확장으로 확보)
대응 상태자가회복 가드 구현 완료PPI-1121 브랜치 커밋 60a8195c, 판정 로직 테스트 14/14 통과, dev 배포 후 iPad 실기기 검증 대기. 근본 대응(clone 축소/제거)은 후속 단계

2. 왜 무음이 발생했는가

2-1. 오디오 경로 — SFU 트랙은 clone의 clone

getUserMedia (iPad 마이크)
  → AudioContext #1 (입력 게인)
  → AudioContext #2 (Gate→EQ→Compressor→Limiter) MediaStreamDestination = processedStream
      ├─ 원본 트랙 → OpenAI pc.addTrack (track.enabled로 mute 제어)   ← 정상이었던 쪽
      └─ clone #1 (mic-relay-stream.ts, 녹음·SFU 공용 — OpenAI mute 격리 목적)
           └─ clone #2 (use-mediasoup-producer.ts:628 — producer 수명 독립 목적)
                → SFU mic-audio producer                              ← 무음이 난 쪽

같은 원본에서 갈라진 두 갈래 중 원본(OpenAI 입력)은 정상 동작하고 clone 갈래만 무음이었다. 즉 마이크·AudioContext·처리체인은 무죄이고, clone 지점에서 갈라진 트랙이 무음으로 태어난다. 아무 이벤트·예외 없이(readyState: live, muted: false) 확률적으로 발생하고, 같은 세션·같은 코드에서 어떤 clone은 정상, 어떤 clone은 무음이므로 애플리케이션 로직이 아니라 브라우저 엔진 레이어의 문제로 판단한다.

2-2. 확진 사례 — 2026-07-09 박서준 (아동·진행자 로그 대조)

07:30:12  [아동] 대기용 mic producer #1 생성 (clone, 정상 — outbound energy 3e-06)
07:30:24  [아동] AI 세션 시작 → mic 스트림 교체(오디오 처리체인 적용)
07:30:26  [아동] mic producer #2 (e42afe5c) 재발행 — 이 clone이 무음으로 태어남 ★근본 원인
07:30:30~45 [아동] 실제 발화 "안녕"/"응" → AI 전사 정상 (원본 트랙은 살아있음)
07:30:34  [아동] 송신측 outbound stats: 발화 중인데 audioEnergyDelta=0 ★송신측 무음 확진
07:30:30  [진행자] consumer 무음 감지 WARN #1 (energy 3e-08)
07:31:28  [진행자] 안 들려서 모니터 새로고침 → 같은 죽은 producer를 재소비
07:31:35  [진행자] 무음 WARN (신고에 인용된 로그) — 새로고침 무효 확인
07:32:09  [진행자] kick #1 → 아동 재입장, 새 producer (b2b9caff)
07:32:49  [아동] 새 producer outbound energy 0.121 = 소리 회복 ★kick으로 회복
07:34~40  활동 전환마다 producer 재-clone (4회) — 무음 WARN 반복되나 전부 무발화 구간이라 판정 불가
07:43:30  [진행자] kick #2 → 07:44:06 진행자 수신 energy 0.047 = 정상 수신 확인
07:47:08  [진행자] 새로고침, 07:51:47 kick #3 → 간헐 재발 의심 (단발 체크라 미확정)
시각(UTC)측정 지점energy delta판정
07:30:34아동 송신 e42afe5c (AI 세션 clone) — 전사로 발화 증명된 창0무음 확진
07:31:35진행자 수신 c6b0c5c1 (새로고침 후 재소비)0무음 (신고 로그)
07:32:49아동 송신 b2b9caff (kick #1 후 새 clone)0.121정상
07:44:06진행자 수신 b307c329 (kick #2 후)0.047정상

핵심 증거: 아동 발화가 OpenAI VAD(Child speech started)와 전사("안녕.", "응.")로 증명되는 창에서 송신측 energy가 0 — 패킷은 나가는데(49pkt/1.7KB ≈ Opus DTX 무음 프레임) 내용물이 디지털 무음이다. 네트워크·SFU·진행자 수신 문제가 아니라 producer에 물린 트랙 자체가 무음이며, 같은 기기·같은 네트워크에서 kick 후 새 clone만 정상인 대조군까지 확보됐다.

2-3. WebKit 공개 이슈·표준과의 대조 — 알려진 버그인가?

가까운 공개 이슈는 존재하지만, 우리 증상과 1:1로 같은 확정 버그로 못 박기는 어렵다.

현재 판단:

2-4. 판별법 (재발 시 확진 절차)

3. 근본적인 해결방안

발생 지점(clone)을 기준으로 ① 발생해도 자동 회복(적용 완료) → ② clone 단계 축소 실험 → ③ clone 제거(근본)의 3단계로 접근한다. ①로 현장 영향을 즉시 제거하고 발생률 데이터를 확보한 뒤 ②·③으로 발생 자체를 없애는 순서.

① 자가회복 가드 적용 완료 (PPI-1121, 60a8195c)

확진 판별식을 런타임 가드로 옮겼다: 발화(OpenAI VAD)가 확인된 측정 창에서 송신 energy 0 → 죽은 clone 확정 → producer 재발행(재-clone = 새 주사위). kick의 회복 효과를 앱 안에서 자동화한 것. 무발화 구간은 판정 보류라 오탐이 없다.

Mic silent guard started                     ← 세션 시작 마커 (튜너블 적용값 포함)
Silent mic clone detected during child speech, recreating producer   ← 무음 감지·재발행
Mic producer audio confirmed after recreate  ← 회복 성공 시그니처
Silent mic clone persists after max recreates ← 2회 실패 → 모니터 오류 알림

한계: 발생 자체를 막지는 못하고, 첫 발화 후 최대 ~10초의 무음 구간이 남는다(측정 2틱 필요).

② producer clone 제거 실험 — clone 단계 축소 실험 후보

현재 clone이 두 번이다: processedAudioStream track → relay clone(#1) → producer clone(#2). iOS WebKit 회피 관점에서 두 번째 producer clone을 제거하고 relay 트랙을 producer에 직접 물리는 것 (produce({ track, stopTracks: false }) + cleanup 수정으로 producer close가 relay 트랙을 stop하지 않게)은 합리적인 실험이다. 특히 무음 로그가 iOS에만 집중된다면 우선순위가 높다.

③ clone 완전 제거 — 소비자별 MediaStreamDestination 분기 근본 대응

소스가 이미 WebAudio 그래프이므로, destination 트랙을 clone하는 대신 처리체인 outputGain 뒤에 소비자별 MediaStreamDestination 3개(OpenAI / SFU / 녹음)를 두면 각 destination이 그래프에서 독립 네이티브 트랙을 직접 생성해 clone() 연산이 아예 사라진다 — 문제 발생 지점 제거.

후속 항목: ai-audio producer stall(bytesSent=0, 활동 전환 시)도 같은 가드 프레임(감지→재발행)에 편입 가능 — 별도 티켓 대상. clone 무음 발생률의 기기 OS별 집계(표본 편향 해소)도 ① 가드 로그로 축적.

4. 관련 문서

분석 작성: 2026-07-13 / chulsu · 원본 로그: 20260709_박서준.json(아동), 20260709_박서준_조이.json(진행자) · 확진 사례: 박서준 14회기 roomId 39231e07-…_14, iPadOS 18.7.9