카메라 인코딩 경량화 — simulcast 고화질 레이어 낭비 개선 개선 방안

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

맥락: 게스트 동시부하 → 영상 끊김(DECODE_OVERLOAD)·오디오 끊김 파일: use-mediasoup-producer.ts / use-mediasoup-consumer.ts / use-guest-media-manager.ts 작성일: 2026-06-17

한 줄 요약

아동 기기는 카메라를 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는 가역적이라 캡처 변경 없이 토글 가능.

기대 효과 & 검증

관련 문서