이번 포스트에서는 웹 애플리케이션에서 비디오 녹화 기능을 구현하고, 녹화된 비디오를 서버에 업로드하는 과정을 설명합니다. 브라우저의 MediaRecorder API와 프리사인 URL(Presigned URL)을 활용하여 효율적인 비디오 업로드 시스템을 구축할 수 있습니다.
비디오 녹화: 실시간으로 사용자의 카메라와 마이크를 활용하여 비디오를 녹화합니다.
녹화된 비디오 업로드: 녹화된 비디오를 서버로 업로드합니다.
프리사인 URL 사용: 서버에서 AWS S3에 파일을 업로드할 수 있는 프리사인 URL을 생성하여 클라이언트가 직접 파일을 업로드할 수 있도록 합니다.
async function startMediaRecording() {
try {
const constraints = {
video: { width: 1280, height: 720 },
audio: true,
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
videoPreview.srcObject = stream;
mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
recordedChunks.push(event.data);
}
};
mediaRecorder.onstop = () => {
videoBlob = new Blob(recordedChunks, { type: 'video/webm' });
recordedChunks = [];
uploadButton.disabled = false;
};
mediaRecorder.start();
startButton.disabled = true;
stopButton.disabled = false;
} catch (error) {
console.error('Error accessing media devices:', error);
}
}
async function uploadVideo() {
try {
if (!videoBlob) {
alert('업로드할 영상이 없습니다.');
return;
}
const payload = {
fileType: videoBlob.type,
fileSize: videoBlob.size,
bucket: '*',
region: '*',
};
const response = await fetch('http://localhost:3000/s3/generate-url', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify(payload),
});
const { presignedUrl, key } = await response.json();
const uploadResponse = await fetch(presignedUrl, {
method: 'PUT',
body: videoBlob,
headers: {
'Content-Type': videoBlob.type,
},
});
if (!uploadResponse.ok) throw new Error('파일 업로드 실패');
alert('파일 업로드 성공!');
sendMetadataToServer(key);
} catch (error) {
alert(`업로드 중 오류 발생: ${error.message}`);
}
}
async function sendMetadataToServer(key) {
const metadataPayload = {
title: titleInput.value || null,
description: descriptionInput.value || null,
thumbnailUrl: thumbnailUrlInput.value || null,
hashtags: hashtagsInput.value ? hashtagsInput.value.split(',') : [],
visibility: visibilityInput.value || null,
videoCode: key,
};
const metadataResponse = await fetch('http://localhost:3000/video', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(metadataPayload),
});
if (metadataResponse.ok) {
alert('영상 업로드 및 메타데이터 저장 성공!');
} else {
console.error('메타데이터 전송 실패:', metadataResponse.status);
alert('메타데이터 전송에 실패했습니다.');
}
}
mediaRecorder.onstop = () => {
videoBlob = new Blob(recordedChunks, { type: 'video/webm' });
recordedChunks = [];
uploadButton.disabled = false;
};
이 코드의 역할은 녹화가 종료된 후 데이터를 Blob 형태로 변환하여 대기 상태로 저장하는 것입니다.
그렇다면 왜 BLob으로 대기상태를 저장하는 걸까요?
녹화 과정에서 MediaRecorder는 비디오 데이터(예: 각 프레임과 오디오 샘플)를 청크(chunk) 단위로 제공합니다.
ondataavailable 이벤트를 통해 녹화 중에 생성된 데이터를 recordedChunks 배열에 저장합니다.
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
recordedChunks.push(event.data);
}
};
녹화가 중지되면 recordedChunks에 여러 데이터 청크가 쌓여 있게 됩니다.
업로드나 미리보기에는 하나의 파일 형식으로 된 데이터가 필요합니다. 이를 위해 Blob(Binary Large Object)을 사용하여 조각들을 하나의 완전한 비디오 파일로 합칩니다.
videoBlob = new Blob(recordedChunks, { type: 'video/webm' });
new Blob():
recordedChunks 배열에 저장된 데이터 청크를 결합하여 하나의 video/webm 형식의 비디오 파일로 변환합니다.
이 Blob 데이터는 업로드 전까지 메모리에 임시로 저장됩니다.