🏃♀️ 들어가며
최근 웹 사이트 상에서의 영상 녹화를 구현할 일이 있었다.
1. 사용자가 웹 사이트 상에서 녹화되는 영상을 실시간으로 확인할 수 있어야 하고
2. 녹화된 영상을 서버에 업로드하거나 다운로드할 수 있어야 했다.
'웹캠 녹화'라는 키워드로 검색하면 웹캠으로 영상을 녹화할 수 있는 사이트들이 몇 개 나오는데 Web API를 활용하면 정말 쉽고 간단하게 그런 사이트들에서 제공하는 기능을 거의 그대로 구현할 수 있다. 이번 글에서는 이 사이트에서의 영상 녹화 기능을 비슷하게 따라 만들어 보면서 영상 녹화를 어떻게 구현했는지 기록해두려고 한다.
🛠️ 구현하기
API(Application Programming Interface)란 개발자가 복잡한 기능을 더 쉽게 만들 수 있도록 프로그래밍 언어로 제공되는 구조체로, 복잡한 코드 대신 개발자가 사용하기 쉬운 문법으로 기능을 구현할 수 있도록 해준다. 사용 가능한 Web API 목록을 보면 정말 다양한 API들이 있는데, 영상 녹화를 위해 MediaRecorder 인터페이스를 사용해보려고 한다.
1. MediaStream 생성하기
MediaRecorder를 사용하려면 녹화할 MediaStream을 전달해줘야 한다. navigator.mediaDevices.getUserMedia()를 통해 웹캠 등 이용 가능한 비디오, 오디오 인풋을 사용한 MediaStream을 생성해준다. 영상 피드백을 실시간으로 보여줄 때 오디오는 나오지 않아도 되기 때문에 비디오 스트림과 오디오 스트림을 따로 생성해서 사용했다.
- videoStream: 실시간 영상 피드백에 사용
- videoStream + audioStream: 영상 녹화를 위한 MediaRecorder에 사용
const VideoRecorder = () => {
const videoRef = useRef<HTMLVideoElement>(null);
const getMediaPermission = useCallback(async () => {
try {
const audioConstraints = { audio: true };
const videoConstraints = {
audio: false,
video: true,
};
const audioStream = await navigator.mediaDevices.getUserMedia(
audioConstraints
);
const videoStream = await navigator.mediaDevices.getUserMedia(
videoConstraints
);
if (videoRef.current) {
videoRef.current.srcObject = videoStream;
}
} catch (err) {
console.log(err);
}
}, []);
useEffect(() => {
getMediaPermission();
}, []);
return (
<video ref={videoRef} autoPlay />
);
};
export default VideoRecorder;
VideoRecorder 컴포넌트를 만들고 pages/index.tsx에서 해당 컴포넌트를 불러주면 아래와 같이 미디어 사용 허가를 묻는 프롬프트가 나오게 된다. Allow를 선택할 경우 video 태그를 통해 실시간 비디오 피드백을 볼 수 있고, Block을 선택할 경우 미디어를 사용할 수 없기 때문에 catch절 안의 구문이 실행된다.


2. MediaRecorder 생성하기
생성한 MediaStream을 가지고 MediaRecorder를 만들어준다. MediaRecorder와 영상 데이터를 담기 위한 변수도 선언해준다.
const VideoRecorder = () => {
const videoRef = useRef<HTMLVideoElement>(null);
const mediaRecorder = useRef<MediaRecorder | null>(null);
const videoChunks = useRef<Blob[]>([]);
const getMediaPermission = useCallback(async () => {
try {
const audioConstraints = { audio: true };
const videoConstraints = {
audio: false,
video: true,
};
const audioStream = await navigator.mediaDevices.getUserMedia(
audioConstraints
);
const videoStream = await navigator.mediaDevices.getUserMedia(
videoConstraints
);
if (videoRef.current) {
videoRef.current.srcObject = videoStream;
}
// MediaRecorder 추가
const combinedStream = new MediaStream([
...videoStream.getVideoTracks(),
...audioStream.getAudioTracks(),
]);
const recorder = new MediaRecorder(combinedStream, {
mimeType: 'video/webm',
});
recorder.ondataavailable = (e) => {
if (typeof e.data === 'undefined') return;
if (e.data.size === 0) return;
videoChunks.current.push(e.data);
};
mediaRecorder.current = recorder;
} catch (err) {
console.log(err);
}
}, []);
useEffect(() => {
getMediaPermission();
}, []);
return (
<video ref={videoRef} autoPlay />
);
};
export default VideoRecorder;
3. 컨트롤 버튼 추가하기
영상 녹화를 시작하고 멈출 수 있는 버튼을 추가한다.
const VideoRecorder = () => {
const videoRef = useRef<HTMLVideoElement>(null);
const mediaRecorder = useRef<MediaRecorder | null>(null);
const videoChunks = useRef<Blob[]>([]);
const getMediaPermission = useCallback(async () => {
try {
const audioConstraints = { audio: true };
const videoConstraints = {
audio: false,
video: true,
};
const audioStream = await navigator.mediaDevices.getUserMedia(
audioConstraints
);
const videoStream = await navigator.mediaDevices.getUserMedia(
videoConstraints
);
if (videoRef.current) {
videoRef.current.srcObject = videoStream;
}
// MediaRecorder 추가
const combinedStream = new MediaStream([
...videoStream.getVideoTracks(),
...audioStream.getAudioTracks(),
]);
const recorder = new MediaRecorder(combinedStream, {
mimeType: 'video/webm',
});
recorder.ondataavailable = (e) => {
if (typeof e.data === 'undefined') return;
if (e.data.size === 0) return;
videoChunks.current.push(e.data);
};
mediaRecorder.current = recorder;
} catch (err) {
console.log(err);
}
}, []);
useEffect(() => {
getMediaPermission();
}, []);
return (
<div>
<video ref={videoRef} className={styles.video} autoPlay />
<button
onClick={() => mediaRecorder.current?.start()}
>
Start Recording
</button>
<button
onClick={() => mediaRecorder.current?.stop()}
>
Stop Recording
</button>
</div>
);
};
export default VideoRecorder;
Stop Recording 버튼을 클릭할 경우 ondataavailable 이벤트 핸들러가 실행되어 videoChunks.current 배열에 Blob 데이터가 담기게 된다.
4. 다운로드 버튼 추가하기
createObjectURL 함수를 이용해 videoChunks.current에 담아준 Blob 데이터를 가리키는 URL을 생성하고 영상을 다운로드할 수 있게 해준다.
const VideoRecorder = () => {
const videoRef = useRef<HTMLVideoElement>(null);
const mediaRecorder = useRef<MediaRecorder | null>(null);
const videoChunks = useRef<Blob[]>([]);
const getMediaPermission = useCallback(async () => {
try {
const audioConstraints = { audio: true };
const videoConstraints = {
audio: false,
video: true,
};
const audioStream = await navigator.mediaDevices.getUserMedia(
audioConstraints
);
const videoStream = await navigator.mediaDevices.getUserMedia(
videoConstraints
);
if (videoRef.current) {
videoRef.current.srcObject = videoStream;
}
// MediaRecorder 추가
const combinedStream = new MediaStream([
...videoStream.getVideoTracks(),
...audioStream.getAudioTracks(),
]);
const recorder = new MediaRecorder(combinedStream, {
mimeType: 'video/webm',
});
recorder.ondataavailable = (e) => {
if (typeof e.data === 'undefined') return;
if (e.data.size === 0) return;
videoChunks.current.push(e.data);
};
mediaRecorder.current = recorder;
} catch (err) {
console.log(err);
}
}, []);
useEffect(() => {
getMediaPermission();
}, []);
const downloadVideo = () => {
const videoBlob = new Blob(videoChunks.current, { type: mimeType });
const videoUrl = URL.createObjectURL(videoBlob);
const link = document.createElement('a');
link.download = `My video - ${dayjs().format('YYYYMMDD')}.webm`;
link.href = videoUrl;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
<div>
<video ref={videoRef} className={styles.video} autoPlay />
<button
onClick={() => mediaRecorder.current?.start()}
>
Start Recording
</button>
<button
onClick={() => mediaRecorder.current?.stop()}
>
Stop Recording
</button>
<button onClick={downloadVideo}>Download</button>
</div>
);
};
export default VideoRecorder;
5. 영상 데이터를 서버에 전송하기
영상 데이터를 서버에 전달해 처리하기 위해서는 Blob 데이터를 multipart/form-data 타입으로 전달하면 된다. 필자는 formidable과 같은 모듈을 이용해 파일을 처리해줬다.
const videoBlob = new Blob(videoChunks.current, { type: 'video/webm' });
const formData = new FormData();
formData.append('video', videoBlob);
📚 참고자료
MDN Web API 문서
MDN Blob 문서
https://bradheo.tistory.com/entry/HTTP-multipartform-data
'웹 개발 > React · Next.js' 카테고리의 다른 글
| [Next.js] 국제화(i18n) 자동화 시스템 구축하기 (0) | 2023.04.30 |
|---|---|
| [번역] useMemo와 useCallback 제대로 알고 사용하기 (0) | 2023.02.28 |
| [React] 전역상태관리 라이브러리 Recoil (0) | 2023.01.30 |
| multer로 AWS S3에 파일 업로드하기 (0) | 2022.10.30 |
| [Next.js] "window is not defined" 에러 해결 (0) | 2022.05.14 |