김태민 3회기 AI 무음 2건 — 수신 패킷은 도착, 재생 샘플 960·0 정지 분석원인 미확정 · 수정 미적용

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

2026-10-14 작성 · 같은 날 갱신(재생 경로·복구 방안) 사건 2026-10-13 21:05–21:08 roomId b3f2a26a-…_3 · Android 10 Chrome 154 · apps/web

요약

사건 ① 아동·진행자 모두 AI 소리를 못 들음 (21:05:52~21:07:09): 아동 브라우저는 AI 음성 패킷을 계속 받았다(packetsReceived 597→1043). 그런데 재생 샘플(totalSamplesReceived)이 960에서 멈췄다. 진행자에게 AI 음성을 넘기는 중계 송출(ai-audio)도 0바이트였다.

사건 ② 진행자는 들리고 아동만 못 들음 (21:07:21~21:07:45): 수신·재생 샘플·중계 송출이 모두 정상이었다. 진행자가 들은 소리는 아동 브라우저를 거쳐 전달되므로 AI 음성은 아동 기기까지 왔다. 남는 곳은 로그 밖의 단말 출력(기기 볼륨·스피커 경로)이다.

이전 문서와의 관계: 변유나 6회기는 packetsReceived가 없어 "SFU가 안 보냈나 / 브라우저가 재생을 못 했나"를 가르지 못했다. 이번 건은 LogRocket webrtc stats에 그 값이 있다. 패킷은 도착했으므로 SFU 미전달은 이 사건에서 기각된다. 디코더 고장도 아니다(손실 보정 0). 남는 것은 브라우저 재생 쪽이 소리를 꺼내 가지 않은 것이고, 이 앱에서 AI 트랙을 꺼내 가는 쪽은 audio 요소 하나다(LiveKit webAudioMix 미사용).

대처: 재현이 안 되므로 원인 확정 대신 감지(패킷 증가·샘플 정지) → 단계 복구 → 단계별 결과 로그로 고객 로그에서 효과와 원인을 함께 검증하는 방안을 제안한다(5절).

패턴: 같은 페이지 안의 AI→AI 활동 전환으로 시작된 세션 5개 중 4개가 무음이었다. 페이지를 새로 로드한 뒤의 첫 세션 3개는 모두 정상이었다. 다만 "새로 로드"는 오디오 요소와 다른 페이지 상태를 함께 초기화하므로, 원인이 요소 재사용인지 다른 상태인지는 아직 가르지 못했다.

1. 시간순 인과

// 2026-10-13 KST · 아동 단말 Android 10, Chrome 154 (모바일) · 진행자 세션 로그 없음(아카이브에 guest만)
21:05:52  AI 세션 c0738878 시작 (같은 페이지, 활동 전환)
          에이전트 패킷 수신 ○ 597 → 1043
          재생 샘플 960 고정  → 아동 스피커 ✗
          중계 송출 0bf2 0바이트 (21:05:58, 재확인 21:06:14) → 진행자 ✗     = 사건 ①
          그 사이 AI 발화 6회 (AI_SPEAKING_START)
21:07:04  페이지 로드 (재입장) → 21:07:08 warm-up → AI 세션 5e8d3238
          재생 샘플 1,416,960 → 2,856,960, 중계 송출 정상
21:07:21  AI "아니, 막 괴롭힐 마음은…" — 아동 못 들음
21:07:35  아동 발화 "소리가 안나."
21:07:45  도움 요청 "소리가 안 들려요" (ISSUE_DIAG: 출력 probe 정상)
21:07:46  중계 송출 audioLevel 0.23 → 진행자는 들음                    = 사건 ②
21:08:14  AI→AI 전환 → 세션 5f7688f6, 중계 송출 7469 0바이트
21:08:25  세션 종료 → 21:08:31 페이지 로드 → 세션 309529c4 정상
21:09:54  AI→AI 전환 → 세션 13e4981a, 재생 샘플 0, 중계 07f8 0바이트
사건 ②의 시각은 진행자 모니터 스크린샷의 말풍선("아니, 막 괴롭힐…", 아동 "소리가 안나.", "응, 그럴 수도 있겠다…")을 세션 로그 전사와 맞춰 확정했다. 사건 ①은 별도 신고 시각이 없어 중계 송출이 멈춘 구간으로 추정했다.

2. 증거 — 받은 패킷 vs 재생 샘플

totalSamplesReceived는 재생 쪽이 꺼내 간 디코딩 샘플 누적이다. 48kHz라 1초에 48,000씩, 30초면 약 1,440,000 늘어난다. 960은 20ms, Opus 프레임 하나다. 첫 프레임만 꺼내고 그 뒤로 꺼내지 않았다는 뜻이다.

AI 세션시각시작 방식packetsReceivedtotalSamplesReceived중계 송출
4cc162bc21:00:43~페이지 로드523→20091.4M→4.3Mf03e 정상
15d1406521:02~21:04AI→AI146→1510960 고정af46 0
790f14ce21:05:10~AI→AI692→13391.4M→2.9M1a47 정상
c073887821:06AI→AI597→1043960 고정0bf2 0
5e8d323821:07:39~페이지 로드680→13071.4M→2.9Mdba7 정상
5f7688f621:08:14~25AI→AI11초라 통계 미기록7469 0
309529c421:09:05~페이지 로드856→16281.4M→2.9M5be2 정상
13e4981a21:10AI→AI489→1316007f8 0

원본 위치

AI audio element health는 무음 세션에서도 정상이었다(paused:false, muted:false, volume:1, currentTimeAdvanced:true). MediaStream 재생은 소리가 없어도 currentTime이 진행할 수 있어 이 로그로는 렌더 정지를 볼 수 없다.

2-1. 수신 통계 읽는 법 — c0738878 21:06:52

inbound는 아동 브라우저가 LiveKit에서 받는 쪽 AI 트랙의 통계(RTCInboundRtpStreamStats)다. 보내는 쪽은 outbound에 따로 있다.

필드값의미
packetsReceived / bytesReceived1043 / 199,608약 21초 분량(패킷당 20ms) 도착 → SFU 전달 정상. 바이트는 압축(Opus) 데이터 크기로 패킷당 약 190바이트
packetsLost / jitter0 / 0.001네트워크 정상
totalSamplesReceived960재생 쪽이 꺼내 간 샘플 개수(바이트 아님). 48kHz라 960 = 20ms = Opus 패킷 1개 분량. 재생 쪽은 10ms(480샘플)씩 요청하므로 요청 2번 뒤 멈춤. 0이면 요청이 아예 없었던 것(13e4981a)
concealedSamples0재생 쪽이 계속 요청하는데 디코딩만 실패했다면 손실 보정 샘플이 늘어야 한다. 0이므로 디코더 고장이 아니라 요청 자체가 없었다
packetsDiscarded1829아무도 꺼내 가지 않아 버려진 패킷. 받은 수보다 큰 것은 audio/red(중복 전송) 조각이 각각 세어진 것으로 추정
audioLevel0재생으로 나간 소리 없음
정상 기대값: 1초에 48,000샘플. 받은 패킷 1043개(약 20.9초)면 약 1,000,000이어야 한다. 이 값은 AI가 말하지 않을 때도 무음 패킷(DTX 미사용)을 꺼내 가므로 계속 늘어난다. 같은 기기 정상 세션은 30초마다 1,411,200~1,416,960씩 늘었다. 출력 장치(스피커)가 꺼져 있어도 이 값은 늘어난다 — 사건 ②가 그 예다.
LiveKit ──패킷──> [수신 ○] → [지터버퍼] ──✗ 요청 없음──> 디코딩·출력 없음
SFU ○  네트워크 ○  디코더 ○(보정 0)  렌더러 꺼내가기 ✗  출력 장치 ○(앞뒤 세션 정상)
WebRTC 수신 쪽은 재생(playout)이 꺼져 있으면 받은 패킷을 ChannelReceive::OnReceivedPayloadData에서 버린다(WebRTC dea374a). "폐기 > 수신, 샘플 정지"와 맞는다.

2-2. 꺼내 가는 쪽 — audio 요소 하나 (Room AudioContext 아님)

방 전환 레이스 문서의 "Room AudioContext가 수신 재생도 멈춘다"는 설명은 현 설정(2.22.4, webAudioMix 미사용)의 수신 쪽에는 해당하지 않는다. 그 문서 작성 시점(2.19.2)의 구조는 확인하지 않았다. 송신(마이크 처리 그래프) 쪽 설명은 이 문서 범위 밖이다.

요소 상태(paused:false, 재생 중)와 내부 렌더러 상태(요청 없음)가 어긋난 것이 관측의 전부다. 무엇이 어긋남을 만들었는지는 미확인이다.

3. 세션 전환 코드 경로

develop 1b9925e3(10/14) 기준. 사건 당일 배포 버전과 다를 수 있다.

stopSession (releaseAudioElement 기본 false)        use-ai-session.ts:1361
 ├ liveKitSession.disconnect()                      :1441
 │   └ TrackUnsubscribed → track.detach(el)         livekit-client-session.ts:3432
 └ pauseAndDetachAudio(): el.pause(); el.srcObject = null   use-ai-audio-output.ts:169-175
startSession
 ├ ensureAudioElement() → 같은 요소 반환           use-ai-audio-output.ts:132-150
 └ TrackSubscribed → audioTrack.attach(el) → play gate  livekit-client-session.ts:3322, :3373
     └ onRemoteAudioStream(new MediaStream([track])) → 진행자 중계 producer  :3402
요소를 새로 만드는 경로재사용하는 경로
페이지 로드 후 "준비 완료" 제스처의 warmUpAudio (use-ai-audio-output.ts:179-213), 전체 정리 cleanupAllSessions (use-guest-page-session.ts:1309), 언마운트 (use-ai-session.ts:2769)새 활동 전환 (use-step-transition.ts:115), restartCurrentSession (use-guest-page-session.ts:809), 활성 세션 위 재시작 (use-ai-session.ts:1611), 시작 실패 정리 (:2317, :2580)
재사용에는 이유가 있다. iPad Safari는 사용자 제스처 안에서 만든 요소만 재생을 허용한다(use-ai-audio-output.ts:177-178 주석). 같은 활동 안의 비AI 스텝은 세션을 끊지 않고 응답 차단만 한다(use-step-transition.ts:236-258).

4. 가설 판정

가설판정근거
SFU·agent가 AI 음성을 안 보냄기각아동 packetsReceived 증가, 서버 트랙 게시 정상
단말 출력 장치 (사건 ①)기각같은 기기·같은 페이지 앞뒤 세션 정상, 출력 probe 정상, 볼륨과 무관한 중계 송출도 0
단말 출력 장치 (사건 ②)유력(로그 밖)수신·재생 샘플·중계 정상인데 아동만 못 들음
pause 직후 약 1ms 만에 attach·play 경합기각 (분석 중 정정)1ms는 "종료 → 시작 함수 호출" 간격. 실제 attach는 방 접속·구독 뒤라 950~1187ms. 정상 세션(968ms)과 차이 없음
오디오 요소 재사용미확정 (상관 있음)무음 4건 모두 재사용 세션, 새 요소 3건 모두 정상. 단 새 요소 = 페이지 재로드라 다른 상태와 분리 안 됨. 재사용 2회째(21:04:39)는 정상
빠른 AI→AI 전환 + Room AudioContext 정지수신 쪽은 약화현 설정에서 AI 트랙은 Room AudioContext를 거치지 않는다(2-2절). "빠른 AI→AI 전환에서만 발생"이라는 조건 자체는 이번에도 맞는다(5건 중 4건)
load() 누락 — 재사용 시 요소 초기화 부족약화 (분석 중 정정)srcObject 대입 자체가 로드 절차를 다시 실행하고(video-audio-loopback.ts:411 주석), srcObject = null은 Chrome 내부 플레이어를 파괴한다(webrtcHacks). 재사용 세션도 이미 매번 비우고 새로 넣으므로 load() 추가 효과는 작을 수 있다
"JS로는 C++ 오디오 파이프라인을 리셋할 수 없고, 네이티브 바인딩이 끊겼다"근거 없음JS의 srcObject 조작이 내부 플레이어 생성·파괴를 직접 일으킨다. new Audio()도 JS 조작인데 새 요소 세션은 정상이었다. 출력 장치 쪽은 공유 자원이라 먹통이었다면 다음 재사용 세션(21:04:39 정상)도 영향을 받았어야 한다
짧은 지연(50~100ms) 후 attach하면 풀린다근거 없음세션 시작 때 이미 약 1초(950~1187ms) 간격으로 비움→attach가 일어났는데도 무음
Chrome은 원격 오디오를 재생 경로가 실제로 렌더할 때만 디코딩하고, 그래야 중계 같은 다른 소비자에도 샘플이 흐른다(추정, 알려진 Chrome 제약). 그래서 재생 샘플 정지와 중계 0바이트가 함께 나타난 것을 하나로 설명할 수 있다.

5. 대처 방안 — 재현 없이 안전하게 재출력 (제안, 미적용)

재현 환경을 만들 수 없으므로 원인을 먼저 확정하지 않는다. 정확히 감지했을 때만 가벼운 복구부터 시도하고, 단계별 결과를 로그로 남겨 고객 로그로 효과와 원인을 함께 검증한다.

감지 — 세션 내내, attach 후 1~2초 유예

1초 간격 2회 연속:
  packetsReceived 증가 > 0   &&   totalSamplesReceived 증가 = 0
  && outputBlocked = false  &&  AI 스텝  &&  visible
// 절대값(≤960) 판정은 세션 중간에 멈추는 경우를 놓친다 → 증가량으로 판정
// 비AI 스텝·출력 차단 중에는 앱이 일부러 play()를 하지 않아 샘플 0이 정상 → 반드시 제외
// 1초 폴링은 playout drift 모니터가 이미 한다: isIdleInterval(livekit-playout-drift-monitor.ts:195, 238-250) + packetsReceived(PPI-1374)

복구 단계 — 단계마다 1.5초 뒤 샘플 증가로 성공 판정

단계동작안전성 · 기대 효과
①같은 요소 재부착: track.detach(el) → track.attach(el) → 재생 게이트 applyLiveKitRemoteAudioPlaybackGate요소·제스처 권한·sinkId 그대로라 회귀 위험 낮음. 효과는 불확실 — 세션 시작 때 이미 한 번 실패한 동작의 반복이다
②요소 교체: 새 Audio()에 attach → play() 성공 그리고 샘플 증가일 때만 교체 확정, 이전 요소 해제Chrome Android는 재생 권한이 페이지 단위라 허용될 가능성 높음. iOS·인앱 WebView는 거부될 수 있으나 검증 후 교체라 손해 없음. 효과는 엇갈림 — 새 요소 세션 3/3 정상이었지만 모두 페이지 재로드와 함께였고, 방 전환 문서의 느린 전환 재사용 3/3도 정상이었다
③아동 화면 "소리 켜기" 버튼 → 탭(제스처) 안에서 요소 생성·attach·play()무음이 풀린 유일한 확인 조건(재로드 + 제스처)을 재로드 없이 재현. LiveKit 공식 패턴(AudioPlaybackStatusChanged + startAudio())과 같다
④진행자 화면 "AI 음성 재연결 필요(재입장 권장)" 알림지금은 사람이 30~60초 뒤에 알아챈다

안전장치

새 요소 생성 시 플랫폼별 재생 권한 (정책 기준, 실기기 미확인)

환경제스처 밖 new Audio()이유
Android·데스크톱 Chrome대체로 허용재생 권한이 페이지 단위(이미 터치함)
Android 인앱 WebView거부 가능mediaPlaybackRequiresUserGesture 기본값이 요소마다 제스처 요구
iOS·iPadOS불확실제스처로 시작한 요소만 허용이 원칙. 단 캡처 중인 페이지의 MediaStream 재생은 자동재생 허용. setSinkId는 제스처 밖에서 거부될 수 있음(PPI-1192)

로그로 검증 (재현 대신)

단계마다 stage, 시도 전후 packetsReceived·totalSamplesReceived 증가량, 재생 게이트 결과, 중계 bytes, UA를 남긴다. "①에서 복구" 비율이 높으면 요소 상태 문제, "②에서 복구"면 요소 객체 문제, "③까지 감"이면 페이지 상태·권한 문제로 원인이 좁혀진다. 권장 순서는 감지·로그 + ① + ③ 먼저, 플랫폼별 play() 거부율을 본 뒤 ② 추가.

6. 이슈 분류기 반영

ppi-issue-classifier 정답지에 두 사례를 등록하고 판정 가드를 추가했다.

사례창정답판정
1013-ai-silent-both21:05:20~21:07:20server.audio적중 (가드 ai_relay_stall_persisted ≥ 1)
1013-ai-silent-child21:06:30~21:08:30child.volume놓침 — 출력측을 보여 주는 로그가 없음
사건 ①의 정답 위치는 분류기에 server.audio로 등록했다. 이번 분석으로 SFU는 패킷을 보냈고 아동 브라우저가 재생하지 않은 것이 확인됐으므로, 위치는 child.audio가 더 맞다. 재등록 여부를 결정해야 한다.

관련 문서