이 글은 Today I Learn 시리즈의 53번째 기록입니다. (총 133개)

캔버스에 그려지는 걸 그대로 영상 파일로 저장하기

소리꽃은 화면(캔버스)에서 돌아가는 파티클 애니메이션을 영상 파일로 뽑아야 했다. 원래는 OBS 같은 화면 녹화로 우회할 생각이었는데, 브라우저에 이미 캔버스를 영상으로 녹화하는 표준 API가 있었다. 서버도, 외부 라이브러리도 없이 두 개의 WebAPI만으로 끝났다 — canvas.captureStream()MediaRecorder.


captureStream — 캔버스를 실시간 스트림으로

canvas.captureStream(fps)를 부르면 그 캔버스가 그려질 때마다 프레임이 담기는 MediaStream이 나온다. 캔버스가 곧 카메라가 되는 셈이다.

const stream = canvas.captureStream(60); // 초당 60프레임으로 캔버스를 캡처

이 스트림 안에는 영상 트랙(video track)이 들어 있다. 우리가 매 프레임 requestAnimationFrame으로 파티클을 그리고 있으니, 그 그림이 그대로 스트림으로 흘러 들어간다.


MediaRecorder — 스트림을 파일로 굳히기

스트림은 흐르기만 할 뿐 파일이 아니다. 그걸 받아 녹화하는 게 MediaRecorder다. 녹화 중에는 데이터가 조각(Blob)으로 넘어오고, 멈추면 조각들을 합쳐 하나의 파일로 만든다.

const rec = new MediaRecorder(stream, {
  mimeType: "video/webm;codecs=vp9",
  videoBitsPerSecond: 14_000_000,
});

const chunks = [];
rec.ondataavailable = (e) => {
  if (e.data.size > 0) chunks.push(e.data);
};
rec.onstop = () => {
  const blob = new Blob(chunks, { type: rec.mimeType });
  // blob을 objectURL로 만들어 <a download>로 내려받으면 저장 끝
};

rec.start();
// ...재생이 끝나면
rec.stop();

포맷은 WebM(VP9/VP8)이다. MP4로 바로 뽑고 싶었지만 브라우저 MediaRecorder의 MP4 지원이 제각각이라, MVP는 WebM으로 두고 필요하면 편집기에서 변환하기로 했다. 어떤 코덱이 되는지는 MediaRecorder.isTypeSupported()로 확인해서 되는 걸 고르면 된다.


소리까지 같이 — 두 스트림을 한 트랙 세트로 합치기

영상만이 아니라 소리도 들어가야 했다. 소리꽃은 사용자가 넣은 오디오 파일을 <audio> 요소로 재생하는데, 미디어 요소에도 captureStream()이 있다. 여기서 오디오 트랙을 꺼내 캔버스의 영상 트랙과 한 MediaStream에 같이 담으면 된다.

const out = new MediaStream();
canvas.captureStream(60).getVideoTracks().forEach((t) => out.addTrack(t));

const audioStream = audioEl.captureStream(); // <audio>의 소리
audioStream.getAudioTracks().forEach((t) => out.addTrack(t));

const rec = new MediaRecorder(out, { mimeType: "video/webm" });

MediaStream은 트랙(영상·오디오)들의 묶음일 뿐이라, 서로 다른 곳에서 온 트랙을 새 스트림에 모아 넣는 게 가능하다. 합성음(오실레이터)은 굳이 파일에 넣지 않고 오디오 파일이 있을 때만 실었다.


녹화할 땐 해상도를 고정한다

화면 캔버스는 창 크기에 맞춰 반응형으로 크기가 바뀐다. 그대로 녹화하면 결과 해상도가 창 크기에 휘둘린다. 그래서 녹화 동안만 캔버스의 백킹 스토어(canvas.width/height)를 원하는 값(예: 1920×1080)으로 고정하고, 그리는 영역도 레터박스 없이 꽉 채우게 바꿨다. 끝나면 원래 반응형으로 되돌린다.

즉 화면에 보이는 크기(style.width)와 실제 픽셀 수(canvas.width)는 별개라, 표시는 컨테이너에 맞춰 축소해 두고 내부 픽셀만 1920×1080으로 잡으면 표시가 흐트러지지 않으면서 영상은 정확히 1080p로 나온다.


배경을 투명하게 — clearRect와 알파 WebM

배경을 검정으로 채울 땐 매 프레임 fillRect로 까맣게 덮었다. 투명 배경(다른 영상 위에 얹을 오버레이용)이 필요하면, 그 자리를 clearRect로 지우기만 하면 캔버스 픽셀에 알파(투명도)가 남는다. VP8/VP9 WebM은 알파 채널을 담을 수 있어서, 이렇게 그린 걸 녹화하면 배경이 투명한 영상이 나온다. 다만 알파 WebM 재생·인코딩 지원은 브라우저마다 편차가 있어 결과 확인이 필요하다.

if (transparent) ctx.clearRect(0, 0, w, h); // 투명
else { ctx.fillStyle = "#000"; ctx.fillRect(0, 0, w, h); } // 검정

실시간 녹화라는 한계

MediaRecorder는 재생을 실시간으로 지켜보며 녹화한다. 그래서 재생 중 프레임이 버벅이면 그 버벅임이 그대로 영상에 담긴다. “한 프레임 한 프레임 완벽하게” 뽑으려면 재생 속도와 무관하게 프레임을 하나씩 렌더해 인코딩하는 방식(ffmpeg.wasm 등)이 필요하다. MVP에선 실시간 녹화로 충분해서 그 최적화는 다음으로 미뤘다.


요약

  • canvas.captureStream(fps)로 캔버스를 실시간 MediaStream으로 만들고, MediaRecorder로 녹화해 WebM Blob으로 저장
  • 미디어 요소(<audio>)에도 captureStream()이 있어, 오디오 트랙을 캔버스 영상 트랙과 새 MediaStream에 함께 담으면 소리까지 녹음
  • 녹화 땐 canvas.width/height(내부 픽셀)를 고정 해상도로, 표시 크기(style)는 별개로 둬서 결과 해상도를 창 크기와 분리
  • 배경은 fillRect(검정) vs clearRect(투명) — 투명이면 VP8/VP9 WebM 알파로 저장(브라우저별 편차)
  • MediaRecorder는 실시간 녹화라 버벅임도 그대로 담긴다 — 프레임 단위 완벽함은 별도 인코딩(ffmpeg.wasm)의 몫

Series: Today I Learn

1 C++ 자료형(Data Type) 2 MD5 vs pHash 3 C++에서 함수의 선언과 정의 4 Tkinter padx, pady 5 메모리와 포인터 변수 6 Call by Value, Call by Reference, Call by Pointer 비교 7 const 8 Gemfile — Jekyll 프로젝트의 의존성 파일 9 kramdown-parser-gfm — Jekyll의 GFM 파서 10 파서(Parser) 11 AHU vs OHU 12 I might try it vs I'll try it 뉘앙스 차이 13 SESSION_EXPIRE_AT_BROWSER_CLOSE=True 14 configuration key 15 Git stash vs discard 16 subprocess.Popen으로 Windows 탐색기에 명령어를 전달 17 Post 잔디 분석하기 18 Google Sheets Sync 최적화 19 DSL (Domain Specific Language)과 GPL (General Purpose Language) 20 마크다운 표 그리는 방법 21 쿼리 파라미터(Query Parameter). 기존 QR코드 재활용 22 Django 보안 취약점 점검 및 수정 23 OOP Object-Oriented Programming 객체 지향 프로그래밍 24 Fernet 대칭 암호화 25 Jekyll 코드블록 안의 Liquid 태그 26 습관만들기 Pawbit TIL 1 27 insertOnConflictUpdate vs DoUpdate(target) 28 세션 필터 29 아코디언(Accordiaon) UI를 펼친상태로 만들기 30 input의 step 31 습관만들기 Pawbit TIL 2 32 습관만들기 Pawbit TIL 3 33 Word Cloud 34 Google Sheets를 데이터 버스로(with AppSheet) 35 Django 모델 텍스트 필드 자동 수집 패턴 36 localStorage로 섹션 토글 상태 유지 37 순차 ID 생성(`select_for_update()` + `max()` 조합) 38 역참조 검색과 distinct() 39 xlsx 다운로드와 로딩 오버레이 충돌 40 Android 파일 공유 MIME 타입 41 AssetManifest — Flutter 빌드 타임 asset 목록 런타임 조회 42 크레센도 Phaser + TypeScript TIL 1 43 크레센도 Phaser + TypeScript TIL 2 44 크레센도 Phaser + TypeScript TIL 3 45 크레센도 Phaser + TypeScript TIL 4 46 크레센도 Phaser + TypeScript TIL 5 47 여둘까 Office Layout TIL 1 48 습관만들기 Pawbit TIL 4 49 여둘까 Office Layout TIL 2 50 여둘까 Office Layout TIL 3 51 크레센도 Phaser + TypeScript TIL 6 52 UTF-8 BOM과 PowerShell 파일 쓰기 53 소리꽃 KeyBloom TIL 1 읽는 중 54 소리꽃 KeyBloom TIL 2 55 소리꽃 KeyBloom TIL 3 56 소리꽃 KeyBloom TIL 4 57 소리꽃 KeyBloom TIL 5 58 습관만들기 Pawbit TIL 5 59 여둘까 Office Layout TIL 4 60 소리꽃 KeyBloom TIL 6 61 소리꽃 KeyBloom TIL 7 62 소리꽃 KeyBloom TIL 8 63 메트로놈 Simple Metronome TIL 1 64 소리꽃 KeyBloom TIL 9 65 메트로놈 Simple Metronome TIL 2 66 소리꽃 KeyBloom TIL 10 67 습관만들기 Pawbit TIL 6 68 크레센도 Phaser + TypeScript TIL 7 69 메트로놈 Simple Metronome TIL 3 70 습관만들기 Pawbit TIL 7 71 정적 블로그 SEO 정비와 Pagefind 검색 도입 72 크레센도 Phaser + TypeScript TIL 8 73 메트로놈 Simple Metronome TIL 4 74 습관만들기 Pawbit TIL 8 75 안드로이드 AudioTrack 연속 재생, 실측 피커 정렬, 카메라 토치 플래시 76 메트로놈 Simple Metronome TIL 5 77 모래게임 Sandrop TIL 1 78 습관만들기 Pawbit TIL 9 79 메트로놈 Simple Metronome TIL 6 80 모래게임 Sandrop TIL 2 81 모래게임 Sandrop TIL 3 82 습관만들기 Pawbit TIL 10 83 습관만들기 Pawbit TIL 11 84 메트로놈 Simple Metronome TIL 7 85 모래게임 Sandrop TIL 4 86 모래게임 Sandrop TIL 5 87 습관만들기 Pawbit TIL 12 88 모래게임 Sandrop TIL 6 89 소리꽃 KeyBloom TIL 11 90 모래게임 Sandrop TIL 7 91 모래게임 Sandrop TIL 8 92 소리꽃 KeyBloom TIL 12 93 모래게임 Sandrop TIL 9 94 소리꽃 KeyBloom TIL 13 95 모래게임 Sandrop TIL 10 96 소리꽃 KeyBloom TIL 14 97 소리꽃 KeyBloom TIL 15 98 소리꽃 KeyBloom TIL 16 99 콘티온 Conti On TIL 1 100 소리꽃 KeyBloom TIL 17 101 콘티온 Conti On TIL 2 102 습관만들기 Pawbit TIL 13 103 콘티온 Conti On TIL 3 104 모래게임 Sandrop TIL 11 105 소리꽃 KeyBloom TIL 18 106 여둘까 Office Layout TIL 5 107 모래게임 Sandrop TIL 12 108 모래게임 Sandrop TIL 13 109 콘티온 Conti On TIL 4 110 모래게임 Sandrop TIL 14 111 소리꽃 KeyBloom TIL 19 112 소리꽃 KeyBloom TIL 20 113 소리꽃 KeyBloom TIL 21 114 소리꽃 KeyBloom TIL 22 115 습관만들기 Pawbit TIL 14 116 모래게임 Sandrop TIL 15 117 모래게임 Sandrop TIL 16 118 소리꽃 KeyBloom TIL 23 119 소리꽃 KeyBloom TIL 24 120 소리꽃 KeyBloom TIL 25 121 소리꽃 KeyBloom TIL 26 122 온실 GreenHouse TIL 1 123 콘티온 Conti On TIL 5 124 모래게임 Sandrop TIL 17 125 소리꽃 KeyBloom TIL 27 126 온실 GreenHouse TIL 2 127 콘티온 Conti On TIL 6 128 모래게임 Sandrop TIL 18 129 콘티온 Conti On TIL 7 130 모래게임 Sandrop TIL 19 131 소리꽃 KeyBloom TIL 28 132 모래게임 Sandrop TIL 20 133 모래게임 Sandrop TIL 21