마지막 업데이트 2026-10-12
증상: 아동이 "소리가 안 들려요" 도움 요청을 반복했다. AI 전사와 말풍선은 정상이었고 AI 음성만 무음이었다. 진행자가 강제퇴장했다.
확정된 범위: agent·TTS·SFU 게시는 정상이었다. 아동 브라우저는 AI 오디오 트랙 구독, 오디오 요소 재생, 연결까지 모두 정상이었다. 그런데 그 트랙의 수신 샘플 수(totalSamplesReceived)가 늘지 않았다. 문제 구간은 SFU → 아동 브라우저 수신·디코드다.
강제퇴장은 원인이 아니다. 퇴장 전부터 무음이었고, 퇴장 후 세션은 정상이었으며, 10:20에 다시 무음이 났다. 13개 세션 중 4개가 무음으로, 세션마다 확률적으로 발생했다.
남은 후보: ① SFU가 RTP를 보내지 않음 ② 브라우저가 디코드·렌더하지 못함 ③ 앱/livekit-client의 트랙 연결 방식이 ②를 유발. packetsReceived를 기록하지 않아 셋을 구분하지 못했다.
// 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 수업 완료 퇴장
| 위치 | 동작 |
|---|---|
apps/web/lib/voice-agent/livekit-client-session.ts:3309-3314 | agent 오디오 트랙을 구독하고 오디오 요소에 연결한 직후 startPlayoutDriftMonitor를 시작한다. receiver는 audioTrack.receiver다. |
livekit-playout-drift-monitor.ts:77-107 readInboundAudioReceiverStats | receiver.getStats()에서 inbound-rtp·audio 항목만 골라 totalSamplesReceived·concealedSamples·지터버퍼 값을 꺼낸다. 실패하면 null을 돌려준다(예외를 밖으로 내보내지 않음). |
livekit-playout-drift-monitor.ts:490 | setInterval로 1초마다 tick을 실행한다. |
livekit-playout-drift-monitor.ts:195, 238-250 isIdleInterval | totalSamplesReceived가 늘지 않으면 유휴로 보고 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. 남는 경로는 마지막뿐이다.
| 항목 | 무음 (10:15:40, 10:16:23, 10:20:24, 10:20:45) | 정상 |
|---|---|---|
| Playout drift monitor started | 4개 모두 기록 | 기록 |
| Playout drift sample | 0건 | 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 health | paused: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 | 없음 |
Failed to flush pending LiveKit ASR continuation window뿐이다. 이 에러는 정상 세션을 포함한 모든 세션 시작 시점에 똑같이 찍혀 무관하다.| 가설 | 판정 | 근거 |
|---|---|---|
| 강제퇴장 | 기각 | 퇴장 전부터 무음, 퇴장 후 정상, 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%) |
현재 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 이벤트 미발생 수정 | 미반영 |
return, livekit-client.esm.mjs:31163-31171). 이번 무음은 "구독은 됐는데 샘플 0"이라 증상이 다르다. PR에도 무음 언급은 없다. Chrome 154 이후 트랙 식별 방식이 바뀌었다는 사실만 공통이다.livekit-client-session.ts:1584-1648 — room.engine, EngineEvent.TransportsCreated, PCTransport 상태 콜백 덮어쓰기 (WebRTC 상태 진단 로그)apps/web/shared/lib/ice-diagnostics-adapters.ts:15, 49-89 — PCTransport private 필드 _pc (ICE 진단)getStats() 결과에서 packetsReceived·bytesReceived·packetsLost·jitterBufferEmittedCount, transport(dtlsState, 선택된 후보 쌍), receiver mid를 함께 기록한다. idle이 AI 발화 중에 계속되면 WARN을 남긴다. 이 값으로 ①②③을 가른다.mute/unmute, TrackMuted, TrackStreamStateChanged를 로깅한다. ISSUE_DIAG에 AI 오디오 요소와 수신 통계를 포함한다.use-mediasoup-producer.ts:229 주석 "업링크/인코더 문제"는 ai-audio에서는 수신 트랙 무음을 뜻할 수 있어 오해 소지가 있다.Playout drift monitor started 이후 다음 stopped까지 Playout drift sample이 0건이면 수신 샘플 0이다. ai-audio Audio producer outbound stalled(bytesSentDelta:0)와 시간대가 맞물리는지 교차 확인한다.Consumer inbound stats의 ai-audio가 bytes null + trackMuted:true./ppi/livekit/prod/agent에서 job별 typecast_tts_synthesis·tts_node_audio_output 완료 확인.service_name="ppi-livekit"의 mediaTrack published. info 레벨이라 downtrack 전달 여부는 보이지 않는다.