브라우저에서 영상의 특정 시점 프레임을 이미지로 추출하려면, HTML의 <video> 태그로 영상을 로드하고, JavaScript를 이용해 원하는 시간으로 이동시킨 뒤, 해당 프레임을 <canvas> 요소에 그린 다음, <canvas>의 내용을 이미지 데이터로 변환하여 저장할 수 있습니다. 이 가이드에서는 이러한 과정을 단계별로 설명합니다.우선, 사용자가 파일을 선택하고 재생할 수 있는 기본적인 HTML 구조를 준비합니다.html<!DOCTYPE html><html lang="ko"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Video Frame Extractor</title> <style> body { font-family: sans-serif; display: flex; flex-direction: column; align-items: center; margin: 20px; } video, canvas { border: 1px solid #ccc; max-width: 800px; margin-top: 20px; } input[type="file"] { margin-bottom: 10px; } .controls { margin-top: 10px; display: flex; gap: 10px; align-items: center; } </style></head><body> <h1>영상 프레임 추출기</h1> <input type="file" id="videoFileInput" accept="video/*"> <video id="videoPlayer" controls muted></video> <div class="controls"> <input type="range" id="timeSlider" min="0" value="0" step="0.01"> <span id="currentTimeDisplay">00:00</span> <button id="captureButton">프레임 추출</button> </div> <canvas id="frameCanvas"></canvas> <a id="downloadLink" download="extracted-frame.png" style="display: none; margin-top: 10px;">다운로드</a> <script src="script.js"></script></body></html>## 1. 영상 로드 및 기본 설정<video> 태그에 src를 설정하여 영상을 로드합니다. 로컬 파일의 경우 URL.createObjectURL을 사용하여 임시 URL을 생성할 수 있습니다. 영상의 메타데이터(길이, 해상도 등)가 로드되면 loadedmetadata 이벤트를 통해 정보를 얻어 시간 슬라이더의 max 값을 설정합니다.ts// script.jsconst videoFileInput = document.getElementById('videoFileInput');const videoPlayer = document.getElementById('videoPlayer');const timeSlider = document.getElementById('timeSlider');const currentTimeDisplay = document.getElementById('currentTimeDisplay');const captureButton = document.getElementById('captureButton');const frameCanvas = document.getElementById('frameCanvas');const downloadLink = document.getElementById('downloadLink');const canvasContext = frameCanvas.getContext('2d');let videoDuration = 0;videoFileInput.addEventListener('change', (event) => { const file = event.target.files[0]; if (file) { const videoURL = URL.createObjectURL(file); videoPlayer.src = videoURL; // 비디오 로드가 완료되면 URL 해제 videoPlayer.onloadedmetadata = () => { videoDuration = videoPlayer.duration; timeSlider.max = videoDuration.toString(); // 슬라이더 최대값을 영상 길이로 설정 updateCurrentTimeDisplay(); // 캔버스 크기를 비디오와 동일하게 설정 frameCanvas.width = videoPlayer.videoWidth; frameCanvas.height = videoPlayer.videoHeight; URL.revokeObjectURL(videoURL); // 사용 후 URL 해제 (메모리 관리) }; }});videoPlayer.addEventListener('timeupdate', updateCurrentTimeDisplay);timeSlider.addEventListener('input', () => { videoPlayer.currentTime = parseFloat(timeSlider.value); updateCurrentTimeDisplay();});function updateCurrentTimeDisplay() { const minutes = Math.floor(videoPlayer.currentTime / 60).toString().padStart(2, '0'); const seconds = Math.floor(videoPlayer.currentTime % 60).toString().padStart(2, '0'); const milliseconds = Math.floor((videoPlayer.currentTime % 1) * 100).toString().padStart(2, '0'); currentTimeDisplay.textContent = `${minutes}:${seconds}.${milliseconds}`; timeSlider.value = videoPlayer.currentTime.toString(); // 슬라이더 위치 업데이트}## 2. 특정 시점 프레임 추출 및 캔버스에 그리기사용자가 프레임 추출 버튼을 클릭하면, videoPlayer.currentTime을 설정하여 원하는 시점으로 영상을 이동시킵니다. 이때 중요한 것은 seeked 이벤트입니다. seeked 이벤트는 영상이 특정 시간으로 이동하여 해당 프레임을 표시할 준비가 완료되었을 때 발생합니다. 이 이벤트 리스너 안에서 canvasContext.drawImage() 메서드를 사용하여 현재 영상 프레임을 캔버스에 그립니다.ts// script.js (이어서)captureButton.addEventListener('click', () => { // 현재 비디오의 시간을 슬라이더 값으로 설정 const targetTime = parseFloat(timeSlider.value); videoPlayer.currentTime = targetTime; // seeked 이벤트는 비디오가 특정 시간으로 이동하여 프레임이 준비되었을 때 발생합니다. videoPlayer.addEventListener('seeked', drawFrameToCanvas, { once: true });});function drawFrameToCanvas() { if (!canvasContext || !videoPlayer) { console.error('Canvas context or video player not available.'); return; } // 캔버스 크기를 비디오와 동일하게 설정 (메타데이터 로드 시 이미 설정했겠지만, 혹시 모를 경우를 대비) frameCanvas.width = videoPlayer.videoWidth; frameCanvas.height = videoPlayer.videoHeight; // 비디오의 현재 프레임을 캔버스에 그립니다. canvasContext.drawImage(videoPlayer, 0, 0, frameCanvas.width, frameCanvas.height); // 추출된 이미지를 다운로드할 수 있도록 준비 prepareDownload(); console.log(`프레임 추출 완료: ${videoPlayer.currentTime}초`);}## 3. 추출된 이미지를 다운로드하기캔버스에 그려진 프레임을 이미지 파일로 변환하려면 canvas.toDataURL() 또는 canvas.toBlob() 메서드를 사용합니다. toDataURL()은 Base64 인코딩된 이미지 데이터를 반환하며, 이를 <a> 태그의 href 속성에 할당하고 download 속성을 함께 사용하여 사용자에게 다운로드 링크를 제공할 수 있습니다. toBlob()은 더 유연하며 큰 이미지에 효율적이지만, 비동기 처리가 필요합니다. 여기서는 간단한 toDataURL 방식을 사용합니다.ts// script.js (이어서)function prepareDownload() { if (!frameCanvas) return; // 캔버스 내용을 PNG 이미지의 Data URL로 변환 const imageDataURL = frameCanvas.toDataURL('image/png'); downloadLink.href = imageDataURL; downloadLink.style.display = 'block'; // 다운로드 링크 표시}## 흔히 하는 실수 및 주의사항* CORS (Cross-Origin Resource Sharing) 문제: 로컬 파일이 아닌 다른 도메인의 영상을 로드하는 경우, 보안상의 이유로 canvas.toDataURL() 또는 toBlob() 사용 시 tainted canvas 오류가 발생할 수 있습니다. 이 문제를 해결하려면 영상 서버에서 CORS 헤더를 올바르게 설정하거나, 영상이 동일 출처(Same-Origin)에 있어야 합니다. video 태그에 crossOrigin="anonymous" 속성을 추가하는 것도 한 방법이지만, 서버 설정이 필수적입니다. 이 글은 클라이언트 측 기술 가이드이므로 서버 설정에 대한 자세한 내용은 다루지 않습니다. (참고: HDomi/frame-pick 프로젝트는 로컬 파일 처리를 전제로 합니다.)* seeked 이벤트의 중요성: videoPlayer.currentTime = targetTime; 직후에 바로 drawImage를 호출하면, 영상이 아직 해당 프레임으로 완전히 이동하지 않았을 수 있습니다. 이 경우 이전 프레임이나 검은 화면이 그려질 수 있으므로, 반드시 seeked 이벤트를 기다린 후에 drawImage를 호출해야 합니다.* 성능 및 메모리: 매우 큰 해상도의 영상이나 빈번한 프레임 추출 작업은 브라우저의 메모리 사용량과 성능에 영향을 줄 수 있습니다. 특히 toDataURL은 Base64 문자열을 생성하므로 큰 이미지의 경우 toBlob을 사용하여 Blob 객체를 직접 다루는 것이 효율적일 수 있습니다. 환경에 따라 성능 차이가 있을 수 있으므로 실제 사용 환경에서 테스트하는 것이 중요합니다.* 비디오 코덱 지원: 브라우저마다 지원하는 비디오 코덱이 다를 수 있습니다. MP4 (H.264), WebM (VP8/VP9) 등 널리 지원되는 포맷을 사용하는 것이 좋습니다.
SYS · POSTING LOG