변유나 6회기 AI 음성만 무음 — 안드로이드 Chrome LiveKit 수신 샘플 0 분석원인 미확정 · 수정 미적용

마지막 업데이트 2026-10-12

2026-10-12 사건 2026-10-11 10:15–10:21 roomId 9338616e-…_6 · apps/web · livekit-client 2.19.2

요약

증상: 아동이 "소리가 안 들려요" 도움 요청을 반복했다. AI 전사와 말풍선은 정상이었고 AI 음성만 무음이었다. 진행자가 강제퇴장했다.

확정된 범위: agent·TTS·SFU 게시는 정상이었다. 아동 브라우저는 AI 오디오 트랙 구독, 오디오 요소 재생, 연결까지 모두 정상이었다. 그런데 그 트랙의 수신 샘플 수(totalSamplesReceived)가 늘지 않았다. 문제 구간은 SFU → 아동 브라우저 수신·디코드다.

강제퇴장은 원인이 아니다. 퇴장 전부터 무음이었고, 퇴장 후 세션은 정상이었으며, 10:20에 다시 무음이 났다. 13개 세션 중 4개가 무음으로, 세션마다 확률적으로 발생했다.

남은 후보: ① SFU가 RTP를 보내지 않음 ② 브라우저가 디코드·렌더하지 못함 ③ 앱/livekit-client의 트랙 연결 방식이 ②를 유발. packetsReceived를 기록하지 않아 셋을 구분하지 못했다.

1. 시간순 인과

// 2026-10-11 KST · 아동 단말: 안드로이드 태블릿 Chrome 155 데스크톱 모드 (UA X11 Linux x86_64, platform Linux armv81)
10:00~10:14  AI 세션 7개 정상 (직행 재시작 포함), drift sample 5초마다 기록
10:14:07     영상 스텝 (91초)
10:15:40     AI 세션 시작 → agent 발화·TTS 정상, SFU 트랙 게시 정상
             → 아동: 트랙 구독 ✓  오디오 요소 재생 ✓  drift monitor started ✓
             → totalSamplesReceived 증가 0 → drift sample 0건
             → 복제 송신 ai-audio bytesSentDelta 0 (stalled WARN)
10:16:17~30  아동 도움 요청 4회
10:16:23     진행자 활동 이동 → 재시작 → 무음
10:16:38     강제퇴장 → 10:16:49 재입장 세션 정상
10:20:24     AI 세션 시작 → 무음 / 10:20:45 재시작 → 무음
10:21:12~31  도움 요청 9회
10:21:27     재시작 → 정상, 이후 도움 요청 멈춤
10:23:22     수업 완료 퇴장

2. 코드 경로 — drift sample이 0건이 되는 이유

위치동작
apps/web/lib/voice-agent/livekit-client-session.ts:3309-3314agent 오디오 트랙을 구독하고 오디오 요소에 연결한 직후 startPlayoutDriftMonitor를 시작한다. receiver는 audioTrack.receiver다.
livekit-playout-drift-monitor.ts:77-107 readInboundAudioReceiverStatsreceiver.getStats()에서 inbound-rtp·audio 항목만 골라 totalSamplesReceived·concealedSamples·지터버퍼 값을 꺼낸다. 실패하면 null을 돌려준다(예외를 밖으로 내보내지 않음).
livekit-playout-drift-monitor.ts:490setInterval로 1초마다 tick을 실행한다.
livekit-playout-drift-monitor.ts:195, 238-250 isIdleIntervaltotalSamplesReceived가 늘지 않으면 유휴로 보고 null을 돌려준다. 로그를 남기지 않는다.
livekit-playout-drift-monitor.ts:428-434유효 샘플 5개마다 Playout drift sample을 기록한다. 정상 세션은 AI가 조용해도 5초마다 남는다(Opus DTX 미사용 → 무음 패킷이 계속 들어옴).

tracker.sample()은 오디오 샘플을 꺼내는 코드가 아니다. 이미 읽은 통계 숫자를 직전 값과 비교하는 계산 함수다. 실제 디코드와 꺼내기는 Chrome 내부(NetEQ → 오디오 출력)가 한다. 모니터는 관찰자일 뿐이라 무음의 원인이 될 수 없다.

로그 없이 null로 빠지는 경로는 셋이다. paused(배제: paused:false), currentTime 역행(배제: 재연결 없음), 수신 샘플 증가 0. 남는 경로는 마지막뿐이다.

3. 증거 — 무음 세션 vs 정상 세션

항목무음 (10:15:40, 10:16:23, 10:20:24, 10:20:45)정상
Playout drift monitor started4개 모두 기록기록
Playout drift sample0건5초마다 (세션당 8~10건/50초)
ai-audio 복제 송신 (use-mediasoup-producer.ts:939 clone)bytesSentDelta:0, audioLevel:0, trackReadyState:live, trackMuted:false (17건 전부)초당 약 2,000바이트
mic-audio 송신초당 2,800~3,900바이트같음
AI audio element healthpaused:false, muted:false, readyState:4, currentTime 진행같음
LiveKit data received (같은 연결)215 / 91건174~252건
Room 연결 상태connecting → connected, 재연결 없음같음
agent (CloudWatch)TTS 완료, tts_node_audio_output 약 4,000ms같음
LiveKit 서버mediaTrack published audio/red같음
ppi-socket 녹음ai stem ffmpeg 비정상 종료, Dropping unreadable/corrupt stem segment없음
로그상 오류 없음: 무음 구간의 error는 Failed to flush pending LiveKit ASR continuation window뿐이다. 이 에러는 정상 세션을 포함한 모든 세션 시작 시점에 똑같이 찍혀 무관하다.

4. 기각된 가설

가설판정근거
강제퇴장기각퇴장 전부터 무음, 퇴장 후 정상, 10:20 재발
ICE/DTLS 연결 실패기각(추정)같은 연결의 데이터 수신 정상, reconnecting 없음. single PC 기본값 기준 판단이며 실제 PC 모드는 미확인
자동 재생 정책기각paused:false, currentTime 진행, audio_play_blocked 0건
시스템 음소거 / 마이크 음소거기각볼륨과 무관한 수신 카운터·복제 송신이 0, 마이크 송신 정상, 아동 조작 없이 회복
트랙 비활성화 (앱 코드)기각setEnabled·setSubscribed 호출 없음, adaptiveStream:false, 복제 트랙 live·unmuted
drift 모니터 내부 예외기각통계 실패는 catch 후 null, 매초 재실행, null이어도 로그는 남음
단말 출력·화면 이탈·CPU기각AUDIO_PROBE 정상, visibilityState:visible, Long Task 0
LiveKit 노드 / agent / 웹 배포기각같은 노드에서 정상·무음 혼재, 10/10과 같은 릴리즈 3.4.8, agent·socket 배포 변경 없음
Chrome 155 단독 원인약함같은 155에서 정상·무음 혼재. 이전 사례는 151·153에서도 발생(8/26, 9/16, 9/29). 10/10(154) 정상 6세션은 표본이 작다(무음률 30%면 6연속 정상 확률 약 12%)

5. livekit-client 관련 조사

현재 livekit-client@2.19.2(apps/web/package.json:64 고정, 7/14 이후 변경 없음). 라이브러리 패치는 없다.

항목내용우리 버전
PR #2089 (2026-09-07 머지, v2.22.3)Chrome 154부터 수신 트랙 track.id가 서버 ID(TR_*)라는 보장이 없어져 mid→trackId 표로 찾아야 한다. dual PC에서 publisher answer의 빈 표가 subscriber offer의 표를 덮어쓰던 버그를 수정(길이 > 0일 때만 교체)미반영 (livekit-client.esm.mjs:21702 무조건 덮어씀)
Issue #2113 / PR #1984 (v2.20.0)2.19 single PC 모드에서 미리 만든 recvonly 섹션 재사용 시 원격 트랙 미구독. 수정은 React Native에서만 섹션 사전 생성을 끔브라우저에는 무관 (최신 버전도 동작 동일)
#2039 (v2.22.0)TrackStreamStateChanged 이벤트 미발생 수정미반영
연관성 한계: #2089의 실패 결과는 "트랙 구독 실패"다(2.19.2 코드상 sid를 못 찾으면 에러 로그 후 return, livekit-client.esm.mjs:31163-31171). 이번 무음은 "구독은 됐는데 샘플 0"이라 증상이 다르다. PR에도 무음 언급은 없다. Chrome 154 이후 트랙 식별 방식이 바뀌었다는 사실만 공통이다.

업데이트 시 확인할 내부 API 의존

6. 개선 제안 (미적용)

  1. 수신 계측 추가: drift 모니터가 이미 읽는 getStats() 결과에서 packetsReceived·bytesReceived·packetsLost·jitterBufferEmittedCount, transport(dtlsState, 선택된 후보 쌍), receiver mid를 함께 기록한다. idle이 AI 발화 중에 계속되면 WARN을 남긴다. 이 값으로 ①②③을 가른다.
  2. 이벤트 공백 메우기: 원격 트랙 mute/unmute, TrackMuted, TrackStreamStateChanged를 로깅한다. ISSUE_DIAG에 AI 오디오 요소와 수신 통계를 포함한다.
  3. livekit-client 2.22.4 업데이트 검토: #2089 포함. dev에서 안드로이드 Chrome 실기기로 재시작 반복 시 무음 비율을 계측과 함께 비교한다.
  4. 집계: S3 세션 로그에서 "drift monitor started 있고 drift sample 0건" 세션을 안드로이드 Chrome 버전별로 센다.
  5. 현장 대응: 진행자가 AI 세션 재시작 또는 강제퇴장하면 대부분 회복된다.
  6. use-mediasoup-producer.ts:229 주석 "업링크/인코더 문제"는 ai-audio에서는 수신 트랙 무음을 뜻할 수 있어 오해 소지가 있다.

7. 판별 방법과 데이터 출처