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

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

iOS WebKit clone track 무음 — 현재 상황 · 원인 · …: 증상: 결론 — 범인부터 공개한다, 원인: 수사 일지 ② — 용의자 선상: 오디오 경로 위의 누구인가, 수정·검증: 수사 일지 ⑤ — 검거: 3단계 해결방안 흐름
동작 흐름 요약
  1. 증상: 결론 — 범인부터 공개한다
  2. 원인: 수사 일지 ② — 용의자 선상: 오디오 경로 위의 누구인가
  3. 수정·검증: 수사 일지 ⑤ — 검거: 3단계 해결방안
iOS(iPad) WebKit에서 MediaStreamTrack.clone()으로 만든 mic 트랙이 확률적으로 live 상태인데 무음(zero-audio)이 되어, AI 대화·전사는 정상인데 진행자(모니터)에게만 아동 목소리가 전달되지 않는 문제의 종합 문서. ① 현재 상황 ② 무음 발생 원인(2026-07-09 박서준 로그 대조 확진 + WebKit 공개 이슈 대조) ③ 근본 해결방안(적용된 자가회복 가드 + clone 축소 실험 + clone 제거)을 정리한다.

결론 — 범인부터 공개한다

범인은 애플리케이션 코드가 아니라 iOS WebKit 엔진 레이어다 — MediaStreamTrack.clone()으로 갈라진 mic 트랙이 확률적으로 readyState: live·muted: false인데 무음(zero-audio)으로 태어난다(표준 위반 동작). 확진 근거는 아동 발화가 VAD·전사로 증명된 창에서 송신측 audioEnergyDelta=0(2026-07-09 박서준 로그 대조). 대응은 3단계 — ① 발화 중 무음 감지 시 producer 재발행하는 자가회복 가드(적용 완료, PPI-1121 60a8195c) ② producer clone 축소 실험 ③ 소비자별 MediaStreamDestination 분기로 clone 자체 제거(근본).

수사 일지 ① — 사건 발생: 진행자에게만 안 들리는 목소리

신고 접수

아동 목소리가 진행자(모니터)에게 들리지 않는다는 신고. 그런데 AI(핑퐁이) 대화와 전사는 정상이었다 — 이 비대칭이 사건의 시그니처다. 관측 사례는 전부 iPad: 7/6 홍서진(CriOS — iOS는 모든 브라우저가 WebKit 엔진 강제), 7/9 박서준(iPadOS 18.7.9). 진행자가 모니터를 새로고침해도 복구되지 않았고, 강제 kick(아동 재입장)만이 소리를 되살렸다. 박서준 세션에서는 새로고침 2회 + kick 3회로 수업 흐름이 반복 중단됐다.

증상아동 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 축소/제거)은 후속 단계
7/8 — 수사 도구 확보

7/6 홍서진 건 초동 수사 때는 소리가 "어디서" 사라지는지 잴 수단이 없었다. 7/8 로깅 범위 확장으로 게스트 송신측 outbound stats와 진행자 수신측 energy 측정이 확보됐고, 바로 다음 날 재발한 박서준 건에서 이 도구가 결정타가 된다.

수사 일지 ② — 용의자 선상: 오디오 경로 위의 누구인가

경로 복원

마이크에서 SFU까지의 오디오 경로를 복원했다. 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은 무음이므로 애플리케이션 로직이 아니라 브라우저 엔진 레이어의 문제로 판단한다.

알리바이 정리

원본 갈래(OpenAI 입력)가 정상이라는 사실이 마이크·AudioContext·처리체인의 알리바이를 세워줬다. 남은 용의자는 clone 분기점뿐. 이제 필요한 것은 "clone 트랙이 정말 무음으로 태어났다"는 물증이다.

수사 일지 ③ — 결정적 단서: 박서준 로그 대조 확진 (2026-07-09)

7/9 — 아동·진행자 로그 대조

7/8에 심어둔 energy 로깅이 처음으로 현행범을 포착했다. 아동측과 진행자측 로그를 같은 타임라인에 겹치자 사건 전체가 재구성됐다.

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만 정상인 대조군까지 확보됐다.

수사 일지 ④ — 전과 조회: WebKit 공개 이슈·표준 대조

알려진 범인인가

엔진 레이어로 좁혀졌으니 다음 질문은 "이미 알려진 버그인가"다. WebKit 공개 이슈 트래커와 표준 문서를 뒤졌다.

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

현재 판단:

판별법 (재발 시 확진 절차)

수사 일지 ⑤ — 검거: 3단계 해결방안

검거 방침

범인이 브라우저 엔진 안에 있으니 우리 손으로 직접 체포할 수는 없다. 대신 발생 지점(clone)을 기준으로 포위망을 좁힌다 — 확진 판별식을 그대로 런타임 가드로 옮겨 현장 피해부터 끊고, clone 횟수를 줄이고, 최종적으로 clone 연산 자체를 없앤다.

발생 지점(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별 집계(표본 편향 해소)도 ① 가드 로그로 축적.

관련 문서

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