조기 입장(대기실) 경로에서 카메라 deviceId 유실 → 가상 카메라로 캡쳐

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

Bug 원인분석 수정 완료 · 원인 ① PPI-1138 chulsu · 2026-07-16

조기 도착으로 미니게임 대기실을 거쳐 자동 입장한 아동에게서, 수업 페이지가 실물 카메라 대신 Mirametrix Virtual Camera(LG gram 번들 Glance 시선추적 SW의 가상 카메라)를 잡아 아동 영상이 캡쳐되지 않은 사례. 근본 원인은 두 겹이다 — ① 대기실 자동입장 콜백(handleWaitingRoomComplete)의 의존성 배열 누락으로 인한 stale closure가 선택한 카메라 deviceIdnull로 저장하고, ② 수업 페이지는 PROBLEMATIC_CAMERAS 필터를 다시 적용하지 않고 deviceId가 없으면 그대로 OS 기본 카메라로 폴백한다. 브라우저(Edge) 문제가 아니며, 제시간에 바로 입장하는 경로에서는 재현되지 않는다.

증상 · 관측

LG gram은 Glance by Mirametrix(시선추적)를 기본 번들로 설치하며, 이것이 OS에 "Mirametrix Virtual Camera"를 등록하고 종종 기본 카메라가 된다. 이 가상 카메라는 백그라운드 앱이 실제 프레임을 넣지 않으면 검은/멈춘 화면을 낸다. 트랙은 readyState:"live"·enabled:true라 앱은 "카메라 켜짐"으로 인식한다.

시간순 인과

06:22:13.028 GuestEntry:SavedUserLoaded → setRoomId(userId) └ 이 render 시점 selectedVideo = null (기기 목록 아직 준비 전) handleWaitingRoomComplete = useCallback([roomId]) 가 이 render의 tryV2Flow( selectedVideo=null )를 캡쳐·고정 06:22:13.317 GuestEntry:VideoDeviceAutoSelected (LG Camera f6719931…) → setSelectedVideo(LG) ← 새 render / 새 tryV2Flow 생성 그러나 roomId 불변 → handleWaitingRoomComplete는 재생성 안 됨 (여전히 옛 tryV2Flow, selectedVideo=null 보유) 06:22:18 GuestEntry:WaitingRoomEntered 조기 도착 → 미니게임 대기실 (~8분) 06:30:00 GuestEntry:WaitingRoomAutoRetry 카운트다운 종료 → 고정된 옛 콜백 실행 → tryV2Flow(selectedVideo=null) → sessionStorage["client-guest-device-ids"].videoDeviceId = null 06:30:07 LOCAL_MEDIA_STREAM getUserMedia videoDeviceId: null → constraints = { video: true } (deviceId 없음 → OS 기본 카메라) → 획득 트랙 label = "Mirametrix Virtual Camera" → 아동 영상 캡쳐 실패 (검은 화면 / 셀프뷰 X)

근본 원인

① 대기실 자동입장 콜백의 stale closure (핵심)

guest-entry.tsx:395-412handleWaitingRoomComplete는 의존성 배열이 [roomId]뿐이라, roomId가 마운트 시(SavedUser) 먼저 세팅된 뒤 콜백이 그 render의 tryV2Flow를 고정한다. 그 시점 selectedVideo는 아직 null. 이후 auto-select로 selectedVideo가 LG 카메라가 되어도 roomId가 안 바뀌므로 콜백은 갱신되지 않는다. 8분 뒤 대기실 카운트다운이 끝나 이 옛 콜백이 실행되면 selectedVideo?.deviceId || null = null이 저장된다.

use-device-manager.ts:33-46PROBLEMATIC_CAMERAS(=Mirametrix) 필터 — 입장 페이지 드롭다운 목록만 손봄
guest-entry.tsx:520 / 533auto-select: setSelectedVideo(prev || videoDevices[0]) + VideoDeviceAutoSelected 트래킹
guest-entry.tsx:395-412handleWaitingRoomComplete = useCallback(…, [roomId])tryV2Flow(roomId) (stale)
guest-entry.tsx:114-121tryV2Flow: sessionStorage["client-guest-device-ids"] = { videoDeviceId: selectedVideo?.deviceId || null, … }

② 수업 페이지는 카메라 필터를 재적용하지 않음

수업 페이지는 PROBLEMATIC_CAMERAS 필터를 다시 돌리지 않고, 저장된 deviceId 문자열만 신뢰해 getUserMedia에 넘긴다. deviceId가 없으면 video: true(=아무 카메라나)로 폴백하므로 OS 기본값인 Mirametrix가 그대로 잡힌다. 즉 필터 결과는 "deviceId 문자열"로만 이어지며, 그 문자열이 null이면 방어막이 전혀 없다.

session/page.tsx:59-71 / 133client-guest-device-ids 읽어 videoDeviceIdGuestLayout에 전달
local-media-constraints.ts:33-38videoDeviceId 없으면 video: { width, height, facingMode } — deviceId 미지정 → 기본 카메라
use-local-media-stream.ts:118-178getUserMedia(constraints) 후 획득 트랙 = Mirametrix, OverconstrainedError 없이 성공(재획득/필터 없음)

재현 조건

이 버그는 "조기 도착 → 미니게임 대기실 → 자동 입장" 경로 + Mirametrix 같은 가상 카메라가 OS 기본 카메라인 기기에서만 재현된다. 제시간에 바로 입장하면 tryV2Flow가 현재 render에서 직접 호출되어(guest-entry.tsx:274 / 356) LG 카메라 deviceId가 정상 저장되므로 가상 카메라에 물리지 않는다.

수정 방향

  1. stale closure 제거: handleWaitingRoomComplete가 최신 선택값을 읽도록 — 선택 device를 ref로 두고 tryV2Flowref.current를 읽거나, 의존성 배열을 보강한다. (근본 원인 ① 해소)
  2. 수업 페이지 방어막 추가: getUserMedia 획득 후 비디오 트랙 labelPROBLEMATIC_CAMERAS에 걸리면 실물 카메라로 재획득하거나 경고를 노출. (deviceId가 null/유실돼도 최후 방어)
  3. 선택: 입장 페이지의 PROBLEMATIC_CAMERAS 상수를 getUserMedia 경로와 공유해 필터 정의를 단일화.

백로그 제목 제안: 조기 입장(대기실 자동입장) 경로에서 선택한 카메라 deviceId가 stale closure로 유실되어 기본 가상 카메라(Mirametrix)로 물리는 버그 수정

수정 (PPI-1138 · commit 3222e299)

원인 ①(stale closure)을 guest-entry.tsxhandleWaitingRoomComplete 의존성 배열에 선택 device 상태를 추가하는 1줄 변경으로 해소했다.

  }, [roomId]);
+ }, [roomId, selectedVideo, selectedAudio, selectedAudioOutput]);

이제 selectedVideo가 auto-select로 LG 카메라가 되면 의존성이 변해 handleWaitingRoomComplete가 재생성되고, 그 콜백은 최신 selectedVideo를 캡쳐한 tryV2Flow를 들게 된다.

부모·자식 조합으로 끝까지 동작하는 이유

이 부모 측 수정이 효과를 내려면 자식 WaitingRoom최신 콜백을 호출해야 하는데, waiting-room.tsx의 카운트다운 effect가 onCountdownComplete를 의존성에 포함하고 있어 두 조각이 올바르게 맞물린다.

부모 guest-entry.tsx:409deps에 selectedVideo/Audio/AudioOutput 추가 → 선택값 변경 시 콜백 재생성(최신 tryV2Flow 캡쳐)
자식 waiting-room.tsx:22-28useEffect(…, [isComplete, onCountdownComplete]) → 콜백 갱신마다 effect 재실행, isComplete 전환 시 최신 콜백setTimeout으로 캡쳐
06:22:13 selectedVideo=LG → handleWaitingRoomComplete 재생성(LG 캡쳐) → WaitingRoom effect도 최신 콜백 보유 06:30:00 isComplete=true → setTimeout(최신 콜백) → tryV2Flow(selectedVideo=LG) → device-ids.videoDeviceId = "LG f6719931…" ✅ 06:30:07 수업 페이지 getUserMedia({deviceId:{exact:"LG…"}}) → LG 카메라 획득 ✅

남는 부분 (이 커밋 범위 밖)

관련 문서

분석 작성: 2026-07-16 / chulsu · 근거: LogRocket 세션 6-019f6996…(아동), roomId 0da83c64-…_2, lessonIndex 2 · Edge 150 / Windows / LG gram · 획득 트랙 label "Mirametrix Virtual Camera"