아동측 미디어·세션 이상 전반 — LogRocket 원인 판별용 로깅 추가 범위 (무음 · 자동전환 멈춤 · 재접속 미디어 연결) 분석로깅 제안

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

작성일: 2026-07-08 사건일: 2026-07-07 (원준호 40회기) room: c14d884f…_40 상태: 🟡 범위 제안 — 코드 미반영

1. 배경 — 왜 필요한가

2026-07-07 원준호 40회기에서 아동(iPhone iOS 26.5 + Chrome/WebKit)이 접속 직후 인트로 영상 단계부터 소리를 전혀 못 들었다. 아동이 도움 메뉴에서 '🙉 소리가 안 들려요'를 3회 신고하고 페이지를 3번 리로드한 끝에(20:36~20:39) 3번째 리로드에서 회복됐다.

문제의 핵심은 관측 공백이었다. 앱 로그는 전 구간 정상이었다 — 인트로 영상 play 이벤트 발생, muted:false, readyState:4, 오디오 워밍업 성공, mic producer 정상. WARN은 iOS에서 상시 발생하는 setSinkId failed 1종뿐(무관 판정, 회복 후에도 발생). 즉 "앱은 소리가 나가고 있다고 믿는데 실제 출력만 무음"인 상태를 로그로 판별할 방법이 전혀 없다.

추정 원인은 WebKit 페이지 오디오 세션이 무음 상태로 시작되는 확률적 현상(리로드로만 회복)이지만, 현재 로깅으로는 이 가설을 확증할 수도, 기기 요인(볼륨/무음 스위치/블루투스 라우팅)과 구분할 수도 없다. 이 문서는 아동측에서 반복 신고되는 이상 전반 — ① 소리 안 들림(무음) ② 영상 재생 종료 후 자동전환이 안 되고 멈추는 현상 ③ 재접속 시 카메라/마이크 연결 문제 — 을 LogRocket에서 원인 판별 가능하게 만드는 로깅 추가 범위를 정의한다. 무음 사건(원준호 40회기)이 계기지만, 같은 관측 공백 구조가 자동전환·재접속 미디어에도 있어 한 번에 범위를 잡는다.

2. 현재 관측 공백 — 미디어 경로별 로깅 현황

경로현재 로깅안 보이는 것
수업 영상 재생
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 미기록
판별의 핵심 신호는 "AudioContext 하드웨어 클럭 전진 여부"다. 프로브용 AudioContext를 생성해 resume() 후 ~250ms 뒤 ctx.currentTime이 0에서 전진하는지 확인하면, 오디오 하드웨어 파이프라인이 실제로 돌고 있는지를 간접 측정할 수 있다. 오디오 세션이 깨진 페이지에서는 state가 running으로 안 가거나 currentTime이 멈춰 있는 서명이 남는다. (JS로 스피커 실출력 자체는 측정 불가 — 이 프로브가 도달 가능한 최하층이다.)

3. 제안 로깅 범위

A AUDIO_OUTPUT_PROBE — 공통 출력 계층 프로브 (핵심)

항목내용
트리거① 준비 확인 직후(기존 워밍업 시점) ② 각 활동의 첫 미디어 재생 시작 시 ③ '소리 계열' 이슈 신고 시(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(). 상시 유지 금지

B GUEST_ISSUE_REPORT 진단 스냅샷 — 신고 시점 원인 캡처

항목내용
트리거도움 메뉴에서 소리 계열('소리가 안 들려요'/'이상한 소리가 들려요') 선택 시. 다른 항목도 경량 스냅샷 동봉 가능
수집 필드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 확장)

C MEET_VIDEO 오디오 헬스 — 영상 재생 확장 필드

항목내용
트리거play 이벤트 시 1회 + 재생 2초 후 1회 (2포인트 델타)
수집 필드기존 필드에 volume 추가, webkitAudioDecodedByteCount 델타(오디오 디코드 진행 = 데이터·디코더 정상 증거), audioTracks 존재, currentTime 전진 여부
로그 이벤트[MEET_VIDEO] audio health — 디코드는 전진하는데 A 프로브가 실패면 "출력 계층 무음" 확정 서명
구현 위치shared/ui/meet-video.tsx (기존 이벤트 로깅 useEffect에 합류)

D AI/TTS 오디오 엘리먼트 헬스 — 핑퐁이 소리 경로

항목내용
트리거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 플레이어 재생부

E 게스트 SFU 송신 스탯 — "소리가 실제로 서버로 나가는가"

항목내용
트리거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 보유 지점)

F 모니터 SFU 수신 스탯 — relay clone 무음 판별 (진행자측)

항목내용
트리거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에 기록되므로 아동·진행자 로그만으로 구간 판별 완결
E+F가 있으면 무음의 구간 절단이 로그만으로 된다: 게스트 audioLevel(입력) → 게스트 bytesSent(송신) → 모니터 bytesReceived(수신) → 모니터 audioEnergy(트랙 에너지) → 재생 엘리먼트 상태. 이 5단계 체인에서 어디까지 정상인지가 곧 원인 구간이다. 서버(Loki) consumer/producer 생성 로그와 조합하면 SFU 내부 vs 클라이언트를 확정 구분할 수 있다.

G 영상 종료 → 자동전환 체인 — "왜 안 넘어갔는지"를 단계별로

기존 분석(영상종료 자동전환 미발생 원인분류 · 게스트 업링크 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, 현재 스텝의 전환 조건, 매칭 결과 — "조건 불일치라 안 넘어간 정상 동작"을 버그와 구분

H 재접속 시 카메라/마이크 재획득 — 실패 사유 분류와 track 수명 추적

현재는 성공 로그만 있어 실패하면 "요청 로그 후 침묵"이다. 실패를 사유별로 분류하고, 성공했더라도 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/활동전환(재소비)으로 회복

4. 판별 매트릭스 — 로그 서명으로 원인 구분

4-1. 소리 안 들림 (무음)

원인기대 로그 서명현재 가능?
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로만 판별 (이번 원준호 케이스)✅ 구조상 분리

4-2. 영상 종료 후 자동전환 멈춤

원인기대 로그 서명현재 가능?
재생 스톨 → 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

4-3. 재접속 시 카메라/마이크 연결 문제

원인기대 로그 서명현재 가능?
장치 점유 (이전 페이지 인스턴스/타 앱)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

5. 주의·제약

createMediaElementSource 재라우팅 금지. 엘리먼트 출력을 WebAudio로 끌어와 레벨을 측정하는 방식은 오디오 경로 자체를 바꿔서 iOS Safari 피치 왜곡 픽스(TTS Blob URL 재생)를 되돌리는 사이드 이펙트 위험이 있다. 측정은 반드시 비침습 프로브(별도 ctx) + 엘리먼트 상태 읽기로 한정한다.

6. 우선순위 제안

순위범위이유
1B 신고 스냅샷 + A 프로브변경 최소·리스크 최소. 신고가 오는 순간 원인 데이터가 LogRocket과 Loki 양쪽에 확보됨 — 이번 사건 재발 시 즉시 판별 가능
2H 재접속 미디어 실패 분류 + track 수명 추적현재 실패가 "요청 후 침묵"이라 원인 분류가 0인 영역 — error.name 한 줄 로깅만으로도 판별력이 즉시 생김. 변경 리스크 최소(로깅만)
3C/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 스텝 메인스레드 정지.