/** * 화면(탭/창) 공유 스트림 위에서 드래그로 영역 선택 → PNG File 반환 */ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { captureErrorMessage, clientRectToVideoPixels, cropVideoFrameToFile, requestDisplayCaptureStream, stopMediaStream, type ScreenCaptureRect, } from '../utils/screenRegionCapture'; interface Props { onCapture: (file: File) => void; onCancel: () => void; } type Phase = 'starting' | 'ready' | 'selecting' | 'processing'; const MIN_DRAG_PX = 8; const ScreenRegionCaptureOverlay: React.FC = ({ onCapture, onCancel }) => { const videoRef = useRef(null); const streamRef = useRef(null); const dragStartRef = useRef<{ x: number; y: number } | null>(null); const [phase, setPhase] = useState('starting'); const [selection, setSelection] = useState(null); const [error, setError] = useState(null); const cleanup = useCallback(() => { stopMediaStream(streamRef.current); streamRef.current = null; }, []); useEffect(() => { let cancelled = false; (async () => { try { const stream = await requestDisplayCaptureStream(); if (cancelled) { stopMediaStream(stream); return; } streamRef.current = stream; const video = videoRef.current; if (!video) { stopMediaStream(stream); onCancel(); return; } video.srcObject = stream; video.muted = true; await video.play(); if (cancelled) return; setPhase('ready'); } catch (e) { if (!cancelled) { setError(captureErrorMessage(e)); setTimeout(onCancel, 2200); } } })(); return () => { cancelled = true; cleanup(); }; }, [cleanup, onCancel]); const finishCancel = useCallback(() => { cleanup(); onCancel(); }, [cleanup, onCancel]); useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') finishCancel(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [finishCancel]); const normalizeRect = (x0: number, y0: number, x1: number, y1: number): ScreenCaptureRect => { const left = Math.min(x0, x1); const top = Math.min(y0, y1); return { left, top, width: Math.abs(x1 - x0), height: Math.abs(y1 - y0), }; }; const handlePointerDown = (e: React.PointerEvent) => { if (phase !== 'ready' && phase !== 'selecting') return; if (e.button !== 0) return; dragStartRef.current = { x: e.clientX, y: e.clientY }; setSelection({ left: e.clientX, top: e.clientY, width: 0, height: 0 }); setPhase('selecting'); (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); }; const handlePointerMove = (e: React.PointerEvent) => { if (!dragStartRef.current) return; const start = dragStartRef.current; setSelection(normalizeRect(start.x, start.y, e.clientX, e.clientY)); }; const handlePointerUp = async (e: React.PointerEvent) => { if (!dragStartRef.current) return; const start = dragStartRef.current; dragStartRef.current = null; const clientRect = normalizeRect(start.x, start.y, e.clientX, e.clientY); if (clientRect.width < MIN_DRAG_PX || clientRect.height < MIN_DRAG_PX) { setSelection(null); setPhase('ready'); return; } const video = videoRef.current; if (!video) { finishCancel(); return; } setPhase('processing'); try { const vidRect = clientRectToVideoPixels(clientRect, video); if (!vidRect) throw new Error('REGION_TOO_SMALL'); const file = await cropVideoFrameToFile(video, vidRect); cleanup(); onCapture(file); } catch (err) { setError(captureErrorMessage(err)); setPhase('ready'); setSelection(null); } }; const hint = phase === 'starting' ? '화면 공유 준비 중…' : phase === 'processing' ? '캡처 처리 중…' : '캡처할 영역을 드래그하세요 · Esc 취소'; return createPortal(