젬파고 개발기 — 브라우저에서 화면공유로 게임 화면 인식하기
시작
Lost Ark 젬 교환 계산기를 만들면서, 게임 화면을 직접 캡처해서 OCR로 젬 정보를 읽어오는 기능이 필요했다. Electron 없이 순수 웹에서 구현하는 게 목표였고, navigator.mediaDevices.getDisplayMedia API를 사용하기로 했다.
화면공유 스트림 → video 태그
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true }); videoRef.current.srcObject = stream; videoRef.current.play();
화면공유를 시작하면 스트림을 <video> 태그에 연결해서 실시간으로 렌더링한다. 공유 종료 시 track.onended 이벤트로 상태를 정리한다.
드래그로 ROI 선택
게임 화면 전체를 OCR 돌리면 노이즈가 많아서, 사용자가 드래그로 관심 영역(ROI)을 지정하도록 했다.
video 위에 canvas를 absolute inset-0으로 겹치고, onMouseDown / onMouseUp 이벤트로 영역을 계산한다.
const scaleX = video.videoWidth / rect.width; const scaleY = video.videoHeight / rect.height; // CSS 렌더 좌표 → 실제 video 픽셀 좌표로 변환
드래그 범위 시각화 — 점선 사각형
드래그하는 동안 선택 범위가 안 보여서 onMouseMove에서 캔버스에 점선 rect를 그리기로 했다.
삽질 1 — canvas 내부 해상도 문제
CSS로 size-full 줘도 canvas의 width/height 어트리뷰트는 기본값 300×150이다. 드로잉 좌표계가 CSS 크기와 달라서 rect가 범위 밖으로 그려졌다.
ResizeObserver로 마운트 시 한 번 동기화해서 해결:
useEffect(() => { const ro = new ResizeObserver(([entry]) => { canvas.width = entry.contentRect.width; canvas.height = entry.contentRect.height; }); ro.observe(canvas); return () => ro.disconnect(); }, []);
삽질 2 — onMouseMove가 버튼 안 눌려도 트리거
onMouseMove는 마우스를 그냥 움직이기만 해도 발생한다. e.buttons === 1 조건을 추가해서 왼쪽 버튼을 누른 상태에서만 그리도록 수정했다.
최종 드로잉 코드
ctx.strokeStyle = "rgba(255, 105, 180, 1)"; ctx.lineWidth = 3; ctx.setLineDash([8, 4]); ctx.strokeRect(startX, startY, currentX - startX, currentY - startY);
CaptureController — 줌 제어 API 탐색
MDN에서 CaptureController가 줌 레벨 제어를 지원한다는 걸 발견했다.
const controller = new CaptureController(); const stream = await navigator.mediaDevices.getDisplayMedia({ controller }); controller.increaseZoomLevel(); // 캡처된 탭 자체를 확대
다만 이건 캡처된 브라우저 탭의 실제 zoom을 변경하는 것이지, 캔버스 위에서 확대하는 게 아니다. Chrome 전용 실험적 기능이고, 브라우저 탭 캡처에서만 동작한다.
현재 구현한 캔버스 crop 방식(ROI를 잘라서 canvas 전체에 확대 렌더링)이 범용성 면에서 더 적합하다고 판단해서 보류했다.
현재 구조 요약
<div> (aspect-video 컨테이너)
<video> ← 화면공유 스트림 렌더링
<canvas> ← 드래그 점선 표시 + ROI 확대 미리보기
canvas 하나로 드래그 UI와 ROI 미리보기를 모두 처리한다. roi가 확정되면 useEffect의 requestAnimationFrame 루프에서 video 프레임을 crop해서 canvas에 확대 렌더링한다.
다음 단계
- ROI 영역을 Tesseract.js로 OCR
- 인식된 텍스트에서 젬 등급/레벨 파싱
- 계산 결과 UI 표시