모의투자 관리 기능 추가
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
/** 화면 캡처 영역 → 업로드용 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 '화면 캡처에 실패했습니다.';
|
||||
}
|
||||
Reference in New Issue
Block a user