한 줄 요약
아동 기기는 카메라를 simulcast 3벌(저·중·고)로 동시 인코딩하는데, 진행자 모니터는 평상시 썸네일(최저 레이어)만 소비한다.
즉 고화질(원본/1080p) 레이어는 포커스해서 크게 볼 때만 쓰이고 평소엔 순수 낭비 인코딩이다.
이 낭비 인코딩이 NVENC/GPU·메인스레드를 점유해 스텝 영상 present(DECODE_OVERLOAD)·Web Audio와 경쟁한다.
→ 고화질 레이어를 평소 끄고 포커스 시에만 켜기(가역적 setParameters) + 캡처 상한으로 인코딩 부하를 줄인다.
배경 — 카메라 인코딩이 부하인 이유
아동 카메라 1장을 진행자에게 보내기 위해 브라우저가 초당 ~30회 실시간 인코딩한다. 현재 구조:
① 캡처 제약 없음 → 기기 기본값(보통 1080p@30fps)
// use-guest-media-manager.ts:335
video: props.videoDevice?.deviceId ? { deviceId: {...} } : false // width/height/frameRate 제한 없음
② simulcast 3개 화질 동시 인코딩
// use-mediasoup-producer.ts:415
encodings: [
{ maxBitrate: 100000, scaleResolutionDownBy: 4 }, // 저 (캡처/4)
{ maxBitrate: 300000, scaleResolutionDownBy: 2 }, // 중 (캡처/2)
{ maxBitrate: 900000 }, // 고 (캡처 원본)
]
즉 1080p + 540p + 270p 세 벌을 동시에 압축 → GPU/CPU 상시 점유.
simulcast 레이어 선택 메커니즘 (이 프로젝트 = 앱 제어)
중요: 3개를 다 인코딩하고, "1개 선택"은 인코딩이 아니라 그 뒤 서버(SFU)/수신측이 한다.
mediasoup는 자동(대역폭 BWE) 또는 앱 제어(setPreferredLayers)가 가능한데, 이 프로젝트는 앱 제어다.
기본값 = 최저 화질(썸네일)
// use-mediasoup-consumer.ts:299 — consumer 생성 시
socket.emit("set-consumer-layers", { consumerId, spatialLayer: 0, temporalLayer: 0 });
포커스/확대 시 = 앱이 명시적으로 상향
// use-mediasoup-consumer.ts:718 setConsumerLayer(peerId, spatialLayer)
socket.emit("set-consumer-layers", { consumerId, spatialLayer, temporalLayer: 2 });
| 진행자 화면 상태 | 선택 레이어 |
| 썸네일/카드뷰 (작게 여러 명) | 최저 (spatialLayer 0) |
| 포커스뷰 (한 명 크게) | 앱 지정 상위 레이어 + 풀프레임(temporal 2) |
| (위에) 수신 대역폭 | 천장 역할 — 모자라면 강등 |
선택 1차 기준은 대역폭이 아니라 진행자 UI 포커스 상태다. (카드뷰/포커스뷰 = Jacob 설계 모니터 대시보드 영역)
핵심 통찰 — 고화질 레이어 "낭비 인코딩"
진행자가 평소 썸네일(layer 0) 로 보는데도 아동 기기는 1080p 고화질 레이어를 항상 인코딩한다.
선택은 서버가 하고 인코딩은 무조건 3벌 다 하기 때문. → 포커스 안 된 평상시의 고화질 레이어 = 소비되지 않는 순수 낭비.
↓이 낭비 인코딩이 NVENC/GPU·CPU·메인스레드를 점유
↓스텝 영상 present(4.17ms@240Hz)·Web Audio가 자원 경쟁에서 밀림
↓영상 끊김(DECODE_OVERLOAD 오라벨)·AI 음성 끊김
경량화 레버
레버 A — 캡처 해상도/프레임레이트 상한
// use-guest-media-manager.ts getUserMedia video 제약 추가
video: { deviceId: {...}, width: { max: 1280 }, height: { max: 720 }, frameRate: { max: 24 } }
1080p→720p면 인코딩 픽셀 절반 이하. 아동 얼굴 모니터링엔 720p(또는 480p)로 충분.
레버 B — simulcast 고화질 레이어 제거
고(1080p) 레이어를 빼서 3벌 → 2벌. 인코딩 부하 약 1/3 감소.
레버 C — 포커스 시에만 고레이어 인코딩 (가역적, 권장)
// sender 측 setParameters — 재협상 없이 런타임 토글
const params = sender.getParameters();
params.encodings[2].active = false; // 고화질 레이어 off (평상시)
await sender.setParameters(params);
// 진행자가 그 아동을 포커스하면 active = true 로 복귀
선택 메커니즘과 정합: 어차피 고레이어는 포커스 시에만 소비되므로, "포커스 신호와 sender 레이어 활성"을 연동하면 화질 손실 0으로 낭비만 제거한다.
적용 시점 — A) 항상(단순·안전) / B) 영상 재생 중만(정밀) / C) 저사양·고부하 기기만. setParameters는 가역적이라 캡처 변경 없이 토글 가능.
기대 효과 & 검증
- 효과: NVENC/CPU 여유 확보 → 스텝 영상 present 마감 확보(DECODE_OVERLOAD 완화) + Web Audio 언더런(오디오 끊김) 간접 완화. 60Hz 안내(기기별 임시방편)와 달리 모든 기기 적용 근본 대응.
- 검증: 고화질 레이어 active=false 전후로 그 기기의 droppedVideoFrames·재생속도(currentTime/wall-clock) 비교 → 67%가 회복되면 카메라 인코딩이 주범임이 확정.
- 주의: 진행자 포커스뷰 화질이 떨어지지 않도록 포커스-레이어 연동 필수. 캡처 상한은 진행자 최대 화질을 720p로 한정함(트레이드오프).