/** 화면 캡처 영역 → 업로드용 PNG File */ export interface ScreenCaptureRect { left: number; top: number; width: number; height: number; } export function clientRectToVideoPixels( rect: ScreenCaptureRect, videoEl: HTMLVideoElement, ): ScreenCaptureRect | null { const bounds = videoEl.getBoundingClientRect(); if (bounds.width <= 0 || bounds.height <= 0) return null; const vw = videoEl.videoWidth; const vh = videoEl.videoHeight; if (vw <= 0 || vh <= 0) return null; const toVidX = (cx: number) => ((cx - bounds.left) / bounds.width) * vw; const toVidY = (cy: number) => ((cy - bounds.top) / bounds.height) * vh; const x1 = Math.max(0, Math.min(vw, Math.round(toVidX(rect.left)))); const y1 = Math.max(0, Math.min(vh, Math.round(toVidY(rect.top)))); const x2 = Math.max(0, Math.min(vw, Math.round(toVidX(rect.left + rect.width)))); const y2 = Math.max(0, Math.min(vh, Math.round(toVidY(rect.top + rect.height)))); const width = x2 - x1; const height = y2 - y1; if (width < 2 || height < 2) return null; return { left: x1, top: y1, width, height }; } export async function cropVideoFrameToFile( videoEl: HTMLVideoElement, rect: ScreenCaptureRect, ): Promise { const canvas = document.createElement('canvas'); canvas.width = rect.width; canvas.height = rect.height; const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('CANVAS_FAILED'); ctx.drawImage( videoEl, rect.left, rect.top, rect.width, rect.height, 0, 0, rect.width, rect.height, ); const blob = await new Promise(resolve => { canvas.toBlob(resolve, 'image/png'); }); if (!blob) throw new Error('BLOB_FAILED'); return new File([blob], `screen-capture-${Date.now()}.png`, { type: 'image/png', lastModified: Date.now(), }); } export async function requestDisplayCaptureStream(): Promise { if (!navigator.mediaDevices?.getDisplayMedia) { throw new Error('DISPLAY_CAPTURE_UNSUPPORTED'); } return navigator.mediaDevices.getDisplayMedia({ video: { displaySurface: 'browser' } as MediaTrackConstraints, audio: false, }); } export function stopMediaStream(stream: MediaStream | null): void { stream?.getTracks().forEach(t => t.stop()); } export function captureErrorMessage(e: unknown): string { if (e instanceof Error) { if (e.name === 'NotAllowedError' || e.message === 'DISPLAY_CAPTURE_UNSUPPORTED') { return '화면 캡처 권한이 거부되었거나 이 브라우저에서 지원하지 않습니다.'; } if (e.message === 'REGION_TOO_SMALL') { return '선택 영역이 너무 작습니다. 다시 드래그해 주세요.'; } if (e.message) return e.message; } return '화면 캡처에 실패했습니다.'; }