마지막 업데이트 2026-07-22
2026-07-07 원준호 40회기에서 아동(iPhone iOS 26.5 + Chrome/WebKit)이 접속 직후 인트로 영상 단계부터 소리를 전혀 못 들었다. 아동이 도움 메뉴에서 '🙉 소리가 안 들려요'를 3회 신고하고 페이지를 3번 리로드한 끝에(20:36~20:39) 3번째 리로드에서 회복됐다.
추정 원인은 WebKit 페이지 오디오 세션이 무음 상태로 시작되는 확률적 현상(리로드로만 회복)이지만, 현재 로깅으로는 이 가설을 확증할 수도, 기기 요인(볼륨/무음 스위치/블루투스 라우팅)과 구분할 수도 없다. 이 문서는 아동측에서 반복 신고되는 이상 전반 — ① 소리 안 들림(무음) ② 영상 재생 종료 후 자동전환이 안 되고 멈추는 현상 ③ 재접속 시 카메라/마이크 연결 문제 — 을 LogRocket에서 원인 판별 가능하게 만드는 로깅 추가 범위를 정의한다. 무음 사건(원준호 40회기)이 계기지만, 같은 관측 공백 구조가 자동전환·재접속 미디어에도 있어 한 번에 범위를 잡는다.
| 경로 | 현재 로깅 | 안 보이는 것 |
|---|---|---|
| 수업 영상 재생 meet-video.tsx (blob URL video 엘리먼트) |
수명주기 이벤트(loadstart/canplay/play/pause), muted, readyState, currentTime, Unexpected pause 복구 | volume, 오디오 트랙 존재/디코드 진행 여부, 실제 출력 여부 |
| 핑퐁이 AI 오디오 (Realtime) use-ai-session.ts (audio 엘리먼트) |
워밍업 성공, DataChannel 수명주기, 재생 시작 | audio 엘리먼트 volume/muted/paused 상태, play() reject 사유, currentTime 전진 여부 |
| TTS 재생 (Blob URL → audio.src) |
재생 큐·청크 수명주기 (TTS 스톨 분석에서 보강됨) | 출력 계층 상태 (아래 공통 항목과 동일) |
| 공통 오디오 출력 계층 (AudioContext / OS 오디오 세션) |
전무 — 워밍업 성공 로그 1건뿐(일회성 권한 프라이밍, 상태 미기록) | AudioContext state(suspended/interrupted), 하드웨어 클럭 전진 여부, 출력 장치 목록/변경, iOS 오디오 세션 상태 |
| 아동 마이크/AI 오디오 → SFU 송신 (mic-audio / ai-audio producer) |
producer 생성/교체/close, transport 상태 — 제어면은 충분 | RTP 실송신량(bytesSent 델타) — producer가 살아 있어도 무음 프레임만 나가는지 판별 불가 |
| SFU → 모니터 수신 (진행자 consumer, monitor-dashboard) |
consumer 생성/close(서버측), 청취 토글 UI | RTP 실수신량·audioLevel — relay clone 확률 무음(전사 정상·소리만 무음)을 로그로 판별 불가, 진행자 audio 엘리먼트 상태도 미기록 |
| 영상 종료 → 자동전환 체인 (guest-video-end / auto-finish) |
MEET_VIDEO ended, VideoEndWatchdog, GuestUplinkStall(업링크 stall 분석 때 추가), 서버 Guest auto-finish | watchdog이 침묵한 사유(reset 조건 발동 시 무기록), emit→서버 ack 왕복, "emit 후 실제 스텝이 전환됐는지" 확인, 재생 중 메인스레드 스톨(timeupdate 공백) |
| 재접속 시 카메라/마이크 재획득 (getUserMedia → track → producer) |
LOCAL_MEDIA_STREAM 요청/성공, DeviceManager 장치 수, producer 생성 | 실패 사유 분류 전무(error.name 미기록), track 수명 이벤트(mute/unmute/ended) 미추적, 재접속 컨텍스트(리로드 루프·이전 인스턴스 장치 점유) 미태깅, devicechange 미기록 |
| 항목 | 내용 |
|---|---|
| 트리거 | ① 준비 확인 직후(기존 워밍업 시점) ② 각 활동의 첫 미디어 재생 시작 시 ③ '소리 계열' 이슈 신고 시(D와 연동) |
| 수집 필드 | ctxStateBefore/After(resume 전후), currentTimeAdvanced(250ms 후 전진 여부, ★핵심), outputLatency, sampleRate, navigator.audioSession?.state(지원 시), 출력 장치 수/라벨(enumerateDevices audiooutput) |
| 로그 이벤트 | [AUDIO_PROBE] output probe result — 실패 서명이면 WARN [AUDIO_PROBE] output suspected silent |
| 구현 방식 | 워밍업과 동일한 일회성 패턴: 프로브 ctx 생성 → resume → 측정 → close(). 상시 유지 금지 |
| 항목 | 내용 |
|---|---|
| 트리거 | 도움 메뉴에서 소리 계열('소리가 안 들려요'/'이상한 소리가 들려요') 선택 시. 다른 항목도 경량 스냅샷 동봉 가능 |
| 수집 필드 | A 프로브 즉시 실행 결과 + 현재 재생 중 미디어 스냅샷(video/audio 엘리먼트별 paused/muted/volume/currentTime/readyState/src종류) + 선택 출력 장치 + visibilityState + AI 세션 활성 여부 |
| 로그 이벤트 | LogRocket: [ISSUE_DIAG] snapshot at report · 서버: 기존 GUEST_ISSUE_REPORT payload에 diag 필드 추가 → Loki에서도 즉시 조회 가능 (이번 조사처럼 LogRocket 세션을 못 찾아도 서버 로그만으로 판별) |
| 구현 위치 | guest-layout.tsx(신고 핸들러) + use-guest-socket.ts:647(sendIssueReport payload 확장) |
| 항목 | 내용 |
|---|---|
| 트리거 | play 이벤트 시 1회 + 재생 2초 후 1회 (2포인트 델타) |
| 수집 필드 | 기존 필드에 volume 추가, webkitAudioDecodedByteCount 델타(오디오 디코드 진행 = 데이터·디코더 정상 증거), audioTracks 존재, currentTime 전진 여부 |
| 로그 이벤트 | [MEET_VIDEO] audio health — 디코드는 전진하는데 A 프로브가 실패면 "출력 계층 무음" 확정 서명 |
| 구현 위치 | shared/ui/meet-video.tsx (기존 이벤트 로깅 useEffect에 합류) |
| 항목 | 내용 |
|---|---|
| 트리거 | AI 오디오 재생 시작 시 + play() reject 시(사유 포함) + 발화 시작 2초 후 1회 |
| 수집 필드 | audio 엘리먼트 paused/muted/volume/currentTime전진/srcObject vs src, reject 시 error.name(NotAllowedError 등) |
| 로그 이벤트 | [AI_SESSION] audio element health / [AI_SESSION] play rejected |
| 구현 위치 | entities/guest-session/model/use-ai-session.ts (워밍업·재생부 인근), TTS 플레이어 재생부 |
| 항목 | 내용 |
|---|---|
| 트리거 | producer 생성/교체 5초 후 1회 + '소리 계열' 신고 시(B와 연동). 주기 폴링 금지 |
| 수집 필드 | pc.getStats()의 outbound-rtp(audio): bytesSent/packetsSent 델타(2포인트), track.audioLevel(media-source, 마이크 입력 에너지), producerId, transport dtlsState/iceState |
| 로그 이벤트 | [MEDIASOUP_PRODUCER] outbound stats — 델타 0이면 WARN outbound stalled (producer 살아있는데 송신 정지 = 업링크/인코더 문제) |
| 판별력 | 마이크 audioLevel 정상 + bytesSent 전진 = 게스트 송신 정상 확정 → 이후 무음은 SFU 또는 수신측으로 좁혀짐 |
| 구현 위치 | 게스트 mediasoup producer 훅 (send transport 보유 지점) |
| 항목 | 내용 |
|---|---|
| 트리거 | consumer 생성/교체 5초 후 1회 + 청취 토글 ON 시 + (선택) 무음 의심 시 재확인 1회 |
| 수집 필드 | inbound-rtp(audio): bytesReceived/packetsReceived 델타, audioLevel/totalAudioEnergy 델타(★수신 트랙에 실제 에너지가 있는지 — relay clone 무음의 결정 신호), packetsLost/jitter, consumerId·대상 아동 peerId, 재생 audio 엘리먼트 paused/muted/volume/sinkId |
| 로그 이벤트 | [MONITOR_CONSUMER] inbound stats — bytesReceived 전진 + audioEnergy 델타≈0이면 WARN consumer suspected silent |
| 판별력 | ①패킷도 안 옴 = SFU 라우팅/서버 문제 ②패킷 오는데 에너지 0 = relay clone 무음 확정(kick/활동전환 재소비로 회복하는 기존 known issue) ③에너지 정상 = 진행자 재생단 문제 |
| 구현 위치 | monitor-dashboard consumer 훅 — 진행자 세션도 LogRocket에 기록되므로 아동·진행자 로그만으로 구간 판별 완결 |
기존 분석(영상종료 자동전환 미발생 원인분류 · 게스트 업링크 stall)에서 VideoEndWatchdog·GuestUplinkStall 로깅이 이미 있으나, "watchdog이 왜 발화하지 않았는지"와 "emit이 실제 전환으로 이어졌는지"는 여전히 로그에 안 남는다. 체인 각 마디의 부정 신호를 채운다.
| 항목 | 내용 |
|---|---|
| watchdog 침묵 사유 | VideoEndWatchdog이 reset/해제될 때 사유를 명시 로깅 (host step change로 reset, 탭 hidden, transport close 등 — 기존 원인분류에서 확인된 침묵 경로들). 이벤트: [VIDEO_END_WATCHDOG] reset { reason } |
| 재생 스톨 감지 | 재생 중 timeupdate가 N초(예: 5초) 이상 공백이면 WARN [MEET_VIDEO] playback stalled { currentTime, duration, readyState } — DECODE_OVERLOAD·메인스레드 정지(iPad 스톨 서명)로 ended가 영영 안 오는 케이스를 실시간 포착 |
| emit ack 왕복 | guest-video-end/auto-finish emit에 ack 콜백 + 타임아웃(예: 3초) — 미도달 시 WARN [AUTO_FINISH] emit ack timeout (GuestUplinkStall과 상호 보강) |
| 전환 적용 확인 | emit 후 T초(예: 5초) 내 activity/step 인덱스 변화 없으면 WARN [AUTO_FINISH] transition not applied { autoTransitionEnabled, 현재 step, 서버 ack 여부 } — "서버까지 갔는데 모니터가 안 넘김"(stale push 계열)과 "클라에서 유실"을 즉시 구분 |
| 조건 스냅샷 | 영상 ended 시점에 자동전환 판정 입력값 1줄 로깅: enabled, 현재 스텝의 전환 조건, 매칭 결과 — "조건 불일치라 안 넘어간 정상 동작"을 버그와 구분 |
현재는 성공 로그만 있어 실패하면 "요청 로그 후 침묵"이다. 실패를 사유별로 분류하고, 성공했더라도 track이 곧 죽는(iOS muted 고착) 케이스를 추적한다.
| 항목 | 내용 |
|---|---|
| getUserMedia 실패 분류 | 실패 시 error.name + constraint + 재시도 회차 로깅: NotReadableError(장치 점유 — 이전 인스턴스/타 앱), NotAllowedError(권한), OverconstrainedError(장치 소실·deviceId 불일치), AbortError/타임아웃. 이벤트: [LOCAL_MEDIA_STREAM] getUserMedia failed { name, constraint, attempt } |
| track 획득 스냅샷 + 수명 이벤트 | 획득 직후 트랙별 readyState/muted/enabled/settings.deviceId 기록, 이후 onmute/onunmute/onended 전부 로깅. muted:true로 시작해 N초 내 unmute가 안 오면 WARN track muted stuck — iOS 재접속 직후 무음 마이크/검은 카메라의 전형 서명 |
| 재접속 컨텍스트 태깅 | 페이지 진입 로그에 navigation.type(reload/navigate)과 단시간 재진입 카운트(예: 5분 내 같은 roomId 재진입 횟수) — 이번 사건 같은 리로드 루프(30초 주기 3회)를 클라 로그에서 즉시 인지, N회 이상이면 WARN re-entry loop detected |
| devicechange 추적 | devicechange 이벤트마다 장치 수 델타 로깅 (BT 헤드셋 연결/해제, 장치 사라짐) — 재접속 전후 장치 구성 변화와 획득 실패의 상관 판별 |
| 권한 상태 | 재획득 직전 navigator.permissions.query(camera/microphone) state 스냅샷(지원 브라우저 한정, 미지원 null) |
| replaceTrack 쌍 로깅 | producer replaceTrack 시 old/new track의 readyState/muted 쌍 기록 — "죽은 트랙으로 교체"가 조용히 지나가는 것 방지 |
// 20:36:26 인트로 재생 시작 — B/C 자동 수집 [MEET_VIDEO] audio health { volume: 1, muted: false, audioDecodedDelta: 68243, currentTimeAdvanced: true } [AUDIO_PROBE] WARN output suspected silent { ctxStateAfter: "interrupted", currentTimeAdvanced: false, outputDevices: 1 } // → "디코드 정상 + 출력 클럭 정지" = 페이지 오디오 세션 무음 확정, 리로드 안내 근거 // 20:36:38 '소리가 안 들려요' 신고 — D 스냅샷이 서버 payload에 동봉되어 Loki에도 기록 GUEST_ISSUE_REPORT { issueText: "소리가 안 들려요", diag: { probe: "silent", video: { playing: true, volume: 1 }, visibility: "visible" } } // (SFU 케이스 예: 모니터 아동 소리 무음 — relay clone이었다면 F가 이렇게 판별) [MONITOR_CONSUMER] WARN consumer suspected silent { peerId: "guest-c14d…_40", bytesReceivedDelta: 41200, audioEnergyDelta: 0.0000 } // → 패킷은 오는데 에너지 0 = relay clone 무음 확정 → kick/활동전환(재소비)으로 회복
| 원인 | 기대 로그 서명 | 현재 가능? |
|---|---|---|
| WebKit 오디오 세션 무음 (이번 케이스 추정) | 미디어 디코드/재생 정상 + 프로브 실패(state≠running 또는 클럭 정지) | ❌ → ✅ A+C |
| autoplay/제스처 차단 | play() rejected: NotAllowedError | △(catch만) → ✅ D |
| 엘리먼트 muted/volume=0 (앱 버그) | 스냅샷에 muted:true 또는 volume:0 | △(muted만) → ✅ B/C/D |
| 블루투스 등 다른 장치로 라우팅 | 프로브 정상 + 출력 장치 목록에 외부 장치 존재/변경 | ❌ → △ A (장치 수·라벨로 정황) |
| 기기 볼륨/무음 스위치 | 모든 로그 정상 (소거법으로만 추정 — JS에서 미관측 영역) | ❌ → △ 소거법 가능해짐 |
| 게스트 송신 정지 (업링크/인코더) | producer 정상인데 bytesSent 델타 0 (E의 outbound stalled WARN) | ❌ → ✅ E |
| SFU 내부 문제 (라우팅/워커) | 게스트 bytesSent 전진 + 모니터 bytesReceived 델타 0 + 서버 consumer 정상 생성 로그 | ❌ → ✅ E+F+Loki |
| relay clone 확률 무음 (모니터, 기존 known issue) | 모니터 bytesReceived 전진 + audioEnergy 델타 ≈ 0 (패킷은 오는데 에너지 없음) | ❌ → ✅ F |
| 진행자 재생단 문제 (autoplay/sink/청취 토글) | audioEnergy 정상 + 재생 엘리먼트 paused/muted 또는 청취 토글 OFF | ❌ → ✅ F |
| 수업 영상은 로컬 blob 재생 | SFU를 경유하지 않음 — 영상 무음은 위 A~C로만 판별 (이번 원준호 케이스) | ✅ 구조상 분리 |
| 원인 | 기대 로그 서명 | 현재 가능? |
|---|---|---|
| 재생 스톨 → ended 미발생 (디코드 과부하·메인스레드 정지) | playback stalled WARN (timeupdate 공백 + currentTime<duration 정지) | △(사후 분석만) → ✅ G |
| watchdog reset으로 침묵 (host step change 등) | [VIDEO_END_WATCHDOG] reset { reason } | ❌ → ✅ G |
| emit 업링크 유실 | ended 정상 + emit ack timeout (+ GuestUplinkStall) | △ → ✅ G |
| 서버 수신됐는데 전환 미적용 (모니터 stale 계열) | 서버 Guest auto-finish 로그 존재 + 클라 transition not applied | ❌ → ✅ G+Loki |
| 조건 불일치/자동전환 OFF (정상 동작) | ended 시점 조건 스냅샷에 enabled:false 또는 매칭 실패 | △ → ✅ G |
| 원인 | 기대 로그 서명 | 현재 가능? |
|---|---|---|
| 장치 점유 (이전 페이지 인스턴스/타 앱) | getUserMedia failed { name: "NotReadableError" } + 직전 re-entry loop 태그 | ❌ → ✅ H |
| 권한 철회/미승인 | NotAllowedError + permission state denied | ❌ → ✅ H |
| 장치 구성 변화 (BT 연결/해제, deviceId 소실) | OverconstrainedError 또는 devicechange 델타와 실패의 시간 일치 | ❌ → ✅ H |
| iOS track muted 고착 (획득은 성공, 무음/검은 화면) | track muted stuck WARN (muted:true 시작 + unmute 미발생) | ❌ → ✅ H |
| track 정상인데 SFU 송신 실패 | H 스냅샷 정상 + E outbound stalled → transport/SFU 구간 | ❌ → ✅ H+E |
| 순위 | 범위 | 이유 |
|---|---|---|
| 1 | B 신고 스냅샷 + A 프로브 | 변경 최소·리스크 최소. 신고가 오는 순간 원인 데이터가 LogRocket과 Loki 양쪽에 확보됨 — 이번 사건 재발 시 즉시 판별 가능 |
| 2 | H 재접속 미디어 실패 분류 + track 수명 추적 | 현재 실패가 "요청 후 침묵"이라 원인 분류가 0인 영역 — error.name 한 줄 로깅만으로도 판별력이 즉시 생김. 변경 리스크 최소(로깅만) |
| 3 | C/D 재생 시작 자동 헬스 + E/F SFU 구간 스탯 + G 자동전환 체인 보완 | 신고 없이도 발생률 측정 가능(무음·스톨 WARN 집계). E/F로 "게스트 송신→SFU→모니터 수신→재생" 구간 절단, G는 기존 VideoEndWatchdog·GuestUplinkStall 위에 침묵 사유·ack·전환확인만 증분 |
| 4 | 자가복구 UX ("소리가 안 나면 새로고침" 자동 안내) | 무음 의심 감지와 연결되는 후속 기능 — UX 변경이라 기획 검토 필요, 로깅 데이터 쌓인 뒤 판단 |
관련 문서·사건: 무음 원인 분석은 2026-07-07 세션 조사(원준호 40회기, LogRocket 아동/진행자 + Loki 교차) 기반. 자동전환 계열은 영상종료 자동전환 미발생 원인분류·watchdog 분석, 게스트 업링크 stall 원인분석·로깅의 후속 보완. 유사 계열: 모니터측 아동 소리 무음(SFU relay clone 확률 무음), TTS 재생 스톨(onended 미발생), iPad AI 스텝 메인스레드 정지.