한 줄 요약
갤럭시 탭 S10 FE가 카메라를 simulcast 3레이어(저·중·고)로 동시 인코딩할 때 실제로 단말 부하가 걸리는지,
그리고 그 부하가 DECODE_OVERLOAD(영상 끊김) 및 socket.io 업링크 stall(자동전환 미반영)과 연관 있는지를
정량 검증하기 위한 로깅 + 1레이어 토글을 use-mediasoup-producer.ts에 추가했다.
레이어별 프레임당 인코딩 시간·CPU 제한 여부·HW 가속 여부를 2초 주기로 기록하고, 테스트 모드 디버그 패널에서
1↔3 레이어를 즉시 전환해 같은 단말에서 A/B 비교한다.
배경 · 가설
아동 단말(S10 FE)은 수업 중 다음을 동시에 수행한다:
- 카메라 simulcast 3레이어 인코딩 (use-mediasoup-producer.ts 카메라 producer, 100k·300k·900k)
- 마이크·AI 오디오 인코딩
- OpenAI Realtime 세션
- 활동 영상 디코딩/재생 (meet-video.tsx)
- WebRTC/mediasoup 송수신
가설: 3레이어 인코딩이 단말 CPU/미디어 코덱 HW를 점유 → ① 활동 영상 디코드를 굶겨 DECODE_OVERLOAD 유발,
② 업링크 대역폭을 잡아먹어 socket.io ack 지연(업링크 stall)에 기여.
추가한 로깅 검증 신호
1. 인코딩 시작 시점 (1회) — [SIMULCAST-LOAD] Camera encoding config
레이어 구성, 원본 트랙 해상도/fps, 단말 식별 정보를 기록한다.
- encodings / layerCount: simulcast 설정
- sourceTrack: 원본 카메라 width/height/frameRate
- userAgent, hardwareConcurrency: 그 단말이 실제 S10 FE인지·CPU 코어 수 확인
2. 주기 측정 (2초마다) — [SIMULCAST-LOAD] Camera encoder stats
producer.getStats()의 outbound-rtp 통계를 레이어(ssrc/rid)별로 읽고, totalEncodeTime·framesEncoded의 구간 증가분으로 프레임당 인코딩 시간을 계산한다(누적값 아님 → 실시간 부하).
| 필드 | 부하 신호 | 의미 |
| cpuLimited | true | 레이어 중 하나라도 qualityLimitationReason === "cpu" → CPU 부족으로 화질 강등 (가장 확실한 부하 신호) |
| totalEncodeMsPerFrame | 값 ↑ | 활성 레이어 전체의 프레임당 인코딩 시간 합(ms) = 단말 총 인코딩 부하 근사치 |
| layers[].powerEfficientEncoder | false | HW 가속 미사용 = SW 인코딩 → CPU 부하 큼 |
| layers[].encoderImplementation | SW 이름 | libvpx·OpenH264 등이면 SW |
| layers[].fps | 목표 대비 ↓ | 30 목표 대비 크게 낮으면 인코딩이 못 따라감 |
| activeLayers | — | 현재 인코딩 중인 레이어 수 (토글 반영 확인용) |
// 부하 걸린 상태 예시
[SIMULCAST-LOAD] Camera encoder stats {
cpuLimited: true, ← 부하 O
totalEncodeMsPerFrame: 28.5, ← 3레이어 합산 부하
activeLayers: 3,
layers: [
{ fps: 12, encodeMsPerFrame: 14.2,
qualityLimitationReason: "cpu",
powerEfficientEncoder: false } ← SW 인코딩 + CPU 제한
]
}
1레이어 전환 토글 A/B 비교
테스트 모드 디버그 패널(GuestDebugPanel)에 Camera Simulcast 1 Layer / 3 Layers 버튼을 추가했다. 클릭하면 카메라 producer를 닫고 새 encodings로 재생성한다.
- 3 Layers(기본): 100k(÷4) · 300k(÷2) · 900k
- 1 Layer: [{ maxBitrate: 900000 }] 단일 고화질만
- 전환 시 [SIMULCAST-LOAD] Switching camera simulcast layers to N 로그
노출 경로: use-mediasoup-producer → use-guest-page-session → guest-layout → GuestDebugPanel. 테스트 모드(isTestMode)에서만 노출되어 운영 게스트에는 보이지 않는다.
검증 절차 (A/B)
- 3레이어 측정: [SIMULCAST-LOAD] Camera encoder stats에서 cpuLimited·totalEncodeMsPerFrame·레이어별 fps를 10~20초 안정화 후 기록
- 1 Layer 버튼 클릭 → producer 재생성
- 1레이어 측정: 같은 로그 재확인 후 비교
주의: 재생성 직후 첫 1~2개 로그는 delta 기준값 리셋으로 encodeMsPerFrame=null이거나 튄다 → 무시. 같은 단말·같은 조명/움직임 조건에서 비교해야 유효(움직임 많으면 인코딩 부하 ↑). activeLayers가 3→1로 바뀌었는지로 토글 반영 확인.
판정: 1레이어에서 cpuLimited:false로 풀리고 totalEncodeMsPerFrame이 크게 하락 → simulcast 3레이어가 부하 원인 확정. 여전히 cpuLimited:true면 → 인코딩 외 요인(디코딩·오디오·Realtime)이 주범.
DECODE_OVERLOAD · 업링크 stall 연관성 분석
코드 확인 결과 공통 근본 원인(단말 리소스 포화)을 공유하지만 코드상 직접 인과는 없는 독립 증상이다.
↔ DECODE_OVERLOAD : 강함 (CPU/코덱 경합)
- DECODE_OVERLOAD(meet-video.tsx:553)는 버퍼는 1초 이상 충분(bufferedAheadSec>1)한데 프레임을 못 따라 디코딩(droppedVideoFrames>10)하는 로컬 디코딩 부하 신호
- utils.ts:44 주석이 직접 명시: "수업 중 WebRTC + 영상 디코드 동시 부하를 못 버텨 DECODE_OVERLOAD 발생"
- Android는 HW 코덱 인스턴스가 제한적 → 인코더 3개 + 활동영상 디코더가 코덱 HW/CPU 경합, SW 폴백 시 CPU 급증 → 디코드 굶김 = DECODE_OVERLOAD 시그니처
↔ socket.io 업링크 stall : 약함/간접 (대역폭 경합)
- 업링크 stall(socket-uplink-stall-detector.ts)은 5초 ack 타임아웃으로 "연결 살았는데 송신 정체"를 잡는 거친 네트워크 신호
- 인코딩은 보통 메인스레드 밖 → CPU 부하가 직접 emit을 5초 막긴 어려움. 연결 고리는 주로 업링크 대역폭: 3레이어(~1.3Mbps) vs 1레이어(~0.9Mbps), 모바일 업링크 좁으면 bufferbloat/혼잡 → ack 지연 가능
교차 검증: LogRocket에서 타임스탬프 매칭 — [SIMULCAST-LOAD] Camera encoder stats의 cpuLimited:true 시점이 Video waiting [DECODE_OVERLOAD] warn과 겹치면 CPU/코덱 경합 공통 원인 확정. 1레이어 토글로 DECODE_OVERLOAD 빈도·업링크 stall(GuestUplinkStall) 이벤트가 함께 줄어드는지 보면 두 연관성을 한 번에 확인.
변경 파일 (브랜치 test/simulcast-load)
| 파일 | 변경 |
| hooks/mediasoup/use-mediasoup-producer.ts | 인코딩 config 로그 + 레이어별 encoder stats 주기 로그 + simulcastLayerCount state + debugSetSimulcastLayers |
| entities/guest-page-session/model/use-guest-page-session.ts | 토글 함수·state 노출 |
| widgets/guest/guest-layout/ui/guest-layout.tsx | 디버그 패널에 props 전달 |
| widgets/guest/guest-layout/ui/guest-debug-panel.tsx | 1↔3 레이어 토글 버튼 UI |