94 lines
2.7 KiB
TypeScript
94 lines
2.7 KiB
TypeScript
/** 화면 캡처 영역 → 업로드용 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<File> {
|
|
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<Blob | null>(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<MediaStream> {
|
|
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 '화면 캡처에 실패했습니다.';
|
|
}
|