PPI Media Observability

수업 중 게스트 카메라·마이크·소리 끊김 진단 로깅 추가 가이드

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

수업 중 게스트 카메라·마이크·소리 끊김 진단 로깅 추가 가이드: 입력: 1. 결론: “게스트 네트워크” 단일 라벨을 5개 원인 축으로 분해, 주요 처리 단계: 3. 권장 로그 파이프라인, 결과: 9. 단계적 적용 제안 흐름
동작 흐름 요약
  1. 입력: 1. 결론: “게스트 네트워크” 단일 라벨을 5개 원인 축으로 분해
  2. 주요 처리 단계: 3. 권장 로그 파이프라인
  3. 결과: 9. 단계적 적용 제안

현재 “아동 카메라/마이크 연결 끊김”, “진행자에게 소리 안 들림”, “아동에게 핑퐁이 소리 안 들림”이 게스트 네트워크 이슈로 묶여 분류되는 경우가 있다. 하지만 실제 게스트 네트워크가 양호한 세션도 있으므로, 네트워크·장치·브라우저 autoplay/출력 라우팅·OpenAI Realtime WebRTC·mediasoup SFU Producer/Consumer·호스트 재생 element를 분리 진단할 수 있도록 로깅을 보강한다.

작성일: 2026-07-24 분석 기준: 로컬 checkout 65e256f / develop 주의: git pull origin develop은 GitHub 인증 문제로 실패
💬 대화로 먼저 이해하기 — "블랙박스 비유" (비개발자·처음 읽는 사람용)
Q이 문서는 무엇을 하자는 문서인가요?
A"아동 소리 안 들림" 같은 문제가 전부 "게스트 네트워크 탓"으로 뭉뚱그려지는 걸 막자는 로깅 보강 가이드예요. 비행기 사고를 무조건 "기상 악화"로 결론 내리지 않도록, 계통마다 블랙박스(기록 장치)를 달자는 거죠.
Q계통이라면 뭐가 있는데요?
A원인 축 5개 — 네트워크/소켓, 게스트 장치·트랙, SFU 송수신, OpenAI Realtime, 호스트 재생. 실제로는 네트워크가 멀쩡한데 마이크 트랙이 죽었거나, 수신은 됐는데 재생 element만 멈춘 경우가 있어서 각 축을 따로 기록해야 해요.
Q기록이 많아지면 오히려 뒤죽박죽되지 않나요?
A그래서 모든 기록에 같은 사건 번호(roomId·peerId·producerId 같은 상관관계 ID)를 붙여 LogRocket과 Grafana를 한 타임라인으로 조인해요. 판정 규칙도 정해뒀어요 — "네트워크"라고 부르려면 여러 증거가 같은 시간대에 함께 나타나야 해요.
Q어디서부터 적용하면 되나요?
A기능을 건드리지 않는 Step 1(비침습 로깅)부터 3단계로 제안돼 있어요. 구체적인 파일·라인은 "7. 코드 기준 상세 작업 위치" 표를 보되, 라인 번호는 로컬 checkout 기준이라 구현 전 최신 develop에서 재확인이 필요해요.

1. 결론: “게스트 네트워크” 단일 라벨을 5개 원인 축으로 분해

P0

네트워크/소켓

Socket.io disconnect/reconnect, engine transport 변경, RTT, candidate pair, TURN/TCP fallback 여부를 수집한다. 단순 navigator.onLine만으로 판단하지 않는다.

P0

게스트 장치/트랙

getUserMedia 성공 후 실제 track readyState, muted, enabled, deviceId/groupId, onmute/onended를 카메라·마이크별로 남긴다.

P0

SFU 송수신

아동 Producer(camera-video/mic-audio/ai-audio)와 진행자 Consumer 생명주기를 producerId/consumerId 기준으로 연결한다.

P1

OpenAI Realtime

AI 세션 RTCPeerConnection/DataChannel과 AI audio element 재생 성공 여부를 분리한다. play().catch(() => {}) 같은 실패 삼킴을 없앤다.

P1

호스트 재생/렌더링

Consumer track은 live인데 host video/audio element가 pause/error/stall 상태인지, audio RMS가 0인지 별도 측정한다.

원칙

상관관계 ID

모든 로그에 roomId, peerId, socketId, producerId, consumerId, streamType, ts를 포함해 LogRocket과 Grafana를 한 줄로 조인할 수 있게 한다.

2. 현재 코드상 관찰된 기존 로그와 빈틈

영역현재 존재하는 로그/이벤트진단상 부족한 점근거 파일
게스트 SFU Producerguest-transport-state 1초 emit, send transport state, RTT 일부, camera/mic/ai producer 생성/trackended/transportclose 로그producer별 outbound stats, bytes/packets 증가량, track mute 지속시간, clone track vs 원본 track 매핑, socketId가 빠짐apps/web/hooks/mediasoup/use-mediasoup-producer.ts:115-154, 284-338, 340-550
진행자 SFU Consumerhost-transport-state 1초 emit, consumer 생성/resume, streamType별 track add 로그consumer별 inbound stats, audio energy/RMS, video framesDecoded/freeze, element playback 상태, producerId-ConsumerId 조인이 부족apps/web/hooks/mediasoup/use-mediasoup-consumer.ts:136-148, 161-260, 287-350, 442-498
OpenAI Realtime AI 세션RTCPeerConnection state와 dataChannel 상태 로그, failed/disconnected/recovered callbackAI audio play() 실패가 삼켜지고, currentTime 증가·audio energy·output sinkId 성공/실패가 부족apps/web/entities/guest-session/model/use-ai-session.ts:1075-1133
레거시 P2P Guest MediauseWebRTCMonitoring으로 track 상태, RTT/packetLoss 일부, video element pause/error/playing/ended 로그socket emit payload가 요약 상태 위주라 packetLoss/RTT/tracks 상세가 monitor/Grafana에서 충분히 보이지 않음apps/web/hooks/use-guest-media-manager.ts:249-380, apps/web/hooks/use-webrtc-monitoring.ts:252-472
Socket/SFU 서버JOIN/LEAVE, duplicate guest cleanup, transport/producer/consumer created/closed 로그server-side mediasoup transport stats/producer score/consumer score가 debug 위주이고 roomId·type·producerId·consumerId 구조화가 약함apps/socket/src/sfu-socket/handlers/connection-handlers.ts:18-152, media-handlers.ts:10-126, mediasoup/*.ts
Monitor 알림media alert 이벤트를 P0/P1/P2로 표시, guest transport degraded 감지알림은 원인 분류가 아니라 상태 표시다. “네트워크”인지 “장치/트랙/consumer/playback”인지 결정할 근거 필드가 부족apps/web/entities/monitor-session/model/use-monitor-session.ts:453-610

3. 권장 로그 파이프라인

01입장 전 환경 스냅샷UA, OS, device list, permission, network info, selected device
02getUserMedia 결과constraints, 성공/실패, track identity, settings/capabilities
03SFU Producer 송신transport state + producer lifecycle + outbound stats delta
04SFU Consumer 수신new-producer → consume → resume → track add → inbound stats
05재생/가청/가시성video frame, audio RMS, element play promise, sinkId, visibility
핵심 판정 규칙: 네트워크 이슈로 분류하려면 최소한 socket disconnect 또는 transport failed/disconnected, RTT/packetLoss 악화, inbound/outbound packets 정체가 같은 시간대에 같이 있어야 한다. 반대로 Producer/Consumer stats는 정상인데 element playback/RMS만 0이면 네트워크가 아니라 브라우저 재생·출력·element 문제로 분류한다.

4. 우선순위별 로깅 추가 포인트

P0-1

공통 mediaDiagnosticLogger 유틸 추가

브라우저 로그는 현재 human-readable이라 LogRocket에서 검색은 가능하지만 구조화 분석이 어렵다. logger.info("MEDIA_DIAG", payload) 형태를 표준화하고, payload는 JSON-safe flat schema로 통일한다.

{
  event: "producer_stats",
  roomId, peerId, socketId,
  role: "guest" | "host",
  streamType: "camera-video" | "mic-audio" | "ai-audio",
  transportId, producerId, consumerId,
  connectionState, iceConnectionState,
  ts: Date.now(),
  seq
}
P0-2

게스트 Producer outbound stats

use-mediasoup-producer.ts의 기존 1초 guest-transport-state 루프에 producer별 stats delta를 추가한다. video는 framesEncoded, qualityLimitationReason, audio는 audioLevel/totalAudioEnergy/bytesSent/packetsSent를 수집한다.

판정 예:
- mic track live + packetsSent 증가 + host inbound 0 → SFU/Consumer 경로 의심
- mic track muted/onended + packetsSent 정체 → 장치/브라우저 track 문제
- RTT/packetLoss 악화 + all streams 정체 → 네트워크 후보
P0-3

진행자 Consumer inbound stats

use-mediasoup-consumer.ts에서 consumer 생성 직후 consumer.id, producerId, streamType을 저장하고, 1~2초 주기로 inbound stats를 streamType별로 남긴다.

audio: packetsReceived, bytesReceived, packetsLost, jitter,
       audioLevel, totalAudioEnergy, concealedSamples
video: framesDecoded, framesDropped, freezeCount,
       totalFreezesDuration, frameWidth/Height
P0-4

서버 SFU lifecycle 구조화

ProducerManager/ConsumerManager/TransportManager 로그에 appData.type, producerPeerId, direction, close reason, dtls/ice selected tuple을 추가한다. 현재는 created/closed 중심이라 원인 분류가 어렵다.

logger.info("SFU_MEDIA_LIFECYCLE", {
  action: "producer_created", roomId, peerId,
  transportId, producerId, kind, streamType: appData?.type
});
P1-1

AI audio element 재생 검증

use-ai-session.ts:1117-1133audioEl.play().catch(() => {})는 실제 무음 원인을 숨긴다. play promise resolve/reject, currentTime 증가, playing/waiting/stalled/error, sinkId 지원 여부를 남긴다.

event: "ai_audio_playback_probe"
fields: playResolved, playErrorName, currentTime,
        paused, muted, volume, sinkIdSupported,
        requestedOutputDeviceId, visibilityState
P1-2

게스트 장치/권한/트랙 이벤트

use-guest-media-manager.ts:333-367와 V2 로컬 미디어 hook에 getUserMedia 전후 로그를 추가한다. 장치 라벨은 권한 후에만 보일 수 있으므로 permission state 변화와 함께 남긴다.

device_snapshot: audioInputs/videoInputs count,
selectedDeviceId hash, groupId hash, label hash,
constraints, track.getSettings(), permission state,
track.onmute/onunmute/onended timestamp
P1-3

Socket.io 연결 원인 세분화

socketSfu.ts:31-74에 이미 reconnect 로그가 있으나 room/peer 맥락이 없다. join 이후에는 socket wrapper가 roomId, peerId, role을 보유해 disconnect/reconnect/connect_error에 같이 찍히도록 한다.

disconnect reason, transport name, previous transport,
reconnect attempt, ping latency, navigator.onLine,
effectiveType/downlink/rtt(지원 브라우저만)
P2

호스트 UI element playback probe

Consumer track이 도착한 뒤 video/audio element가 실제 렌더링·가청 상태인지 남긴다. 진행자가 “소리 안 들림”을 신고할 때 Consumer stats는 정상인데 element만 muted/paused일 수 있다.

video: readyState, paused, videoWidth/Height,
       requestVideoFrameCallback delta
host audio: HTMLAudioElement 상태 또는 WebAudio RMS,
            output device/sink 상태

5. 분류 매트릭스: 어떤 로그 조합이면 네트워크가 아닌가

관찰 조합분류해석다음 확인
socket/transport connected, guest outbound packets 증가, host inbound packets 증가, host audio RMS 0호스트 재생/출력네트워크가 아니라 audio element mute/volume/sink/OS 출력/브라우저 autoplay 가능성host playback probe, output device, tab mute
guest mic track muted/onended, outbound audio packets 정체, socket은 정상게스트 장치/브라우저마이크 권한·장치 전환·iOS/Chrome track 중단 가능성track event, getSettings, permission/devicechange
camera outbound framesEncoded 정체, mic outbound 정상카메라/인코더전체 네트워크 장애가 아니라 카메라 track 또는 video encoder/freezeframesEncoded, qualityLimitationReason, track readyState
guest outbound 정상, server producer created 정상, host consume/resume 실패Consumer/SFU 수신진행자 Consumer 또는 recv transport 문제consumerId, producerId, canConsume, resume response
RTT 급증/packetLoss 증가, socket reconnect, producer/consumer packets 모두 정체네트워크 강한 후보이 경우에만 “게스트 네트워크” 라벨을 1순위로 유지candidate pair, TURN/TCP fallback, 시간대 Grafana
AI RTCPeerConnection failed, SFU mic/camera 정상OpenAI Realtime 경로진행자가 아동 음성은 듣지만 AI/프리셋/핑퐁이 상호작용만 깨질 수 있음dataChannel readyState, OpenAI pc stats, session events

6. 구현 체크리스트

7. 코드 기준 상세 작업 위치

파일/라인추가할 로그목적
apps/web/hooks/mediasoup/use-mediasoup-producer.ts:115-154send transport state 변화에 socketId, transportId, reconnectAttempt, durationSinceLastConnected 추가transport 이슈와 일시적 reconnect를 구분
apps/web/hooks/mediasoup/use-mediasoup-producer.ts:284-338기존 RTT 루프에 producer별 outbound stats delta 추가네트워크 문제인지 track/encoder 문제인지 분리
apps/web/hooks/mediasoup/use-mediasoup-producer.ts:340-550camera/mic/ai track 원본+clone id, onmute/onunmute/onended, producer id, streamType 구조화장치 track 중단, iPad remote audio clone 이슈 추적
apps/web/hooks/mediasoup/use-mediasoup-consumer.ts:161-260consume 요청/응답에 producerId, consumerId, streamType, resume latency, failure reasonProducer는 있는데 Consumer가 실패하는 케이스 분리
apps/web/hooks/mediasoup/use-mediasoup-consumer.ts:442-498recv transport RTT 루프에 consumer별 inbound stats delta 추가진행자 “소리 안 들림/화면 멈춤” 수신단 근거 확보
apps/web/entities/guest-session/model/use-ai-session.ts:1078-1097OpenAI pc stats snapshot, dataChannel state, failure/recovery durationOpenAI Realtime 실패와 SFU 실패 분리
apps/web/entities/guest-session/model/use-ai-session.ts:1117-1133AI audio play promise 결과, sinkId, currentTime 증가, audio element events“아동에게 핑퐁이 소리 안 들림”의 재생/출력 원인 추적
apps/web/hooks/use-guest-media-manager.ts:333-380getUserMedia constraints/result, track settings, startTrackMonitoring status payload 확장레거시 P2P 경로의 장치/트랙 이슈 분리
apps/web/socketSfu.ts:31-74room/peer 맥락이 포함된 connect_error/disconnect/reconnect log소켓 레벨 네트워크 문제와 앱 세션 상태 연결
apps/socket/src/mediasoup/transportManager.ts:68-84dtlsstatechange, close reason, direction, peerId, roomId, transportId 구조화서버에서 transport close/fail 원인 확인
apps/socket/src/mediasoup/producerManager.ts:57-69producer score/trace, appData.type, close reason, consumer count아동 Producer 자체 문제인지 판단
apps/socket/src/mediasoup/consumerManager.ts:93-115consumer score/producerclose/transportclose에 producerPeerId, streamType, roomId 추가진행자 Consumer만 닫히는 케이스 추적
apps/web/entities/monitor-session/model/use-monitor-session.ts:453-610media alert payload에 diagnosisSummary, sourceLayer, correlatedIds 추가알림에서 “네트워크 추정” 대신 근거 기반 분류 표시

8. 권장 이벤트 이름

클라이언트 LogRocket용

MEDIA_DIAG device_snapshot
MEDIA_DIAG get_user_media_result
MEDIA_DIAG track_state_change
MEDIA_DIAG sfu_transport_state
MEDIA_DIAG producer_lifecycle
MEDIA_DIAG producer_stats
MEDIA_DIAG consumer_lifecycle
MEDIA_DIAG consumer_stats
MEDIA_DIAG element_playback_state
MEDIA_DIAG ai_realtime_state
MEDIA_DIAG socket_connection_state

서버 Grafana/Loki용

SFU_MEDIA_LIFECYCLE transport_created|connected|closed|dtls_failed
SFU_MEDIA_LIFECYCLE producer_created|closed|score
SFU_MEDIA_LIFECYCLE consumer_created|resumed|closed|producer_closed|score
SFU_ROOM_LIFECYCLE join|leave|duplicate_cleanup|disconnect_cleanup

9. 단계적 적용 제안

Step 1

비침습 로깅

공통 schema와 Producer/Consumer stats logging만 먼저 추가한다. 기능 동작을 바꾸지 않고 진단 데이터부터 확보한다.

Step 2

Monitor 진단 표시

transport-only alert가 아니라 “송신 정상/수신 정체/재생 정체” 같은 diagnosisSummary를 진행자 화면에 표시한다.

Step 3

자동 복구

로그로 분류 기준이 안정되면 특정 조건에서 producer re-produce, consumer re-consume, audio element 재생 재시도 등 복구 액션을 붙인다.

주의: 로깅 추가 전에는 “게스트 네트워크 문제”로 확정하지 말고 “네트워크/장치/SFU/재생 경로 미분리” 상태로 표기하는 것이 안전하다. 특히 좋은 네트워크에서도 iOS/Chrome 장치 track 중단, remote audio clone 무음, host output routing 문제가 발생할 수 있다.