Canvas captureStream
Canvas에 그림을 그리거나 비디오 프레임을 합성하는 것까지는 할 수 있다. 그런데 이걸 녹화하거나, WebRTC로 실시간 전송하거나, <video> 태그에서 재생하려면 어떻게 해야 할까?
Canvas는 픽셀 단위로 직접 그리는 즉시 모드 렌더링이다. 프레임이라는 개념이 없고, toDataURL()이나 toBlob()으로 정지 이미지 하나를 추출하는 것만 가능하다. 동영상처럼 연속된 프레임을 스트림으로 뽑아내는 방법이 없었다.
captureStream()은 이 간극을 메운다. Canvas의 렌더링 결과를 MediaStream 객체로 변환해서, 브라우저의 미디어 파이프라인(MediaRecorder, WebRTC, <video> 등)에 바로 연결할 수 있게 해준다.
기본 사용법
const canvas = document.querySelector('canvas');
const stream = canvas.captureStream(30); // 30 FPS로 캡처
이게 전부다. captureStream()을 호출하면 MediaStream이 반환되고, 이 스트림 안에는 CanvasCaptureMediaStreamTrack 하나가 들어 있다. 이 트랙이 Canvas의 내용을 실시간으로 비디오 프레임으로 변환한다.
반환된 MediaStream은 웹캠에서 getUserMedia()로 얻는 스트림과 동일한 인터페이스다. 그래서 웹캠 스트림을 쓸 수 있는 곳이면 어디든 Canvas 스트림도 쓸 수 있다.
frameRate 파라미터
captureStream()의 유일한 파라미터인 frameRate는 세 가지 모드로 동작한다.
값을 지정하는 경우
const stream = canvas.captureStream(25); // 초당 25프레임 캡처
지정한 FPS에 맞춰 자동으로 프레임을 캡처한다. Canvas에 변화가 없어도 동일한 프레임이 반복 캡처된다. 녹화나 WebRTC 전송처럼 일정한 프레임 레이트가 필요할 때 사용한다.
주의할 점은 이 값이 상한선이라는 것이다. captureStream(60)이라고 해서 반드시 60fps가 나오는 건 아니다. Canvas에 실제로 그리는 속도가 30fps면 캡처되는 프레임도 30fps가 상한이 된다. 그리고 음수 값을 넣으면 NotSupportedError가 발생한다.
값을 생략하는 경우
const stream = canvas.captureStream(); // 변경 감지 모드
Canvas 내용이 변경될 때마다 자동으로 새 프레임을 캡처한다. 변경이 없으면 프레임을 생성하지 않기 때문에 리소스를 절약할 수 있다. 간헐적으로 업데이트되는 Canvas(예: 차트, 화이트보드)에 적합하다.
"변경"의 감지 기준은 브라우저 구현에 따라 다르다. 일반적으로 drawImage(), fillRect() 같은 Canvas 2D API 호출이 발생하면 변경으로 인식한다.
0을 지정하는 경우
const stream = canvas.captureStream(0); // 수동 모드
자동 캡처를 완전히 끈다. 프레임을 캡처하려면 트랙의 requestFrame() 메서드를 직접 호출해야 한다.
const stream = canvas.captureStream(0);
const track = stream.getVideoTracks()[0];
function renderLoop() {
// Canvas에 뭔가를 그린다
ctx.drawImage(video, 0, 0);
ctx.fillText('워터마크', 10, 30);
// 수동으로 프레임 캡처
track.requestFrame();
requestAnimationFrame(renderLoop);
}
정확한 타이밍에 프레임을 캡처해야 하거나, 렌더링 루프와 캡처 타이밍을 완전히 동기화하고 싶을 때 유용하다. 예를 들어 복잡한 합성 작업에서 모든 레이어를 다 그린 후에만 프레임을 캡처하고 싶다면 수동 모드가 적합하다.
CanvasCaptureMediaStreamTrack
captureStream()이 반환하는 스트림에 포함된 트랙은 일반 MediaStreamTrack이 아니라 CanvasCaptureMediaStreamTrack이라는 특수한 서브클래스다. 두 가지 고유한 속성/메서드가 있다.
const stream = canvas.captureStream(0);
const track = stream.getVideoTracks()[0];
// canvas 속성: 이 트랙이 캡처하고 있는 Canvas 요소 참조
console.log(track.canvas); // <canvas> 요소
// requestFrame(): 수동으로 프레임 하나를 캡처
track.requestFrame();
requestFrame()은 frameRate를 0으로 설정했을 때 주로 사용하지만, 0이 아닌 경우에도 호출 가능하다. 자동 캡처 사이에 즉시 프레임을 하나 더 캡처하고 싶을 때 쓸 수 있다.
실전 활용
Canvas 녹화
가장 대표적인 사용 사례다. Canvas에서 실시간으로 합성한 결과를 동영상 파일로 저장한다.
const canvas = document.querySelector('canvas');
const stream = canvas.captureStream(30);
const recorder = new MediaRecorder(stream, {
mimeType: 'video/webm;codecs=vp9'
});
const chunks = [];
recorder.ondataavailable = (e) => {
if (e.data.size > 0) chunks.push(e.data);
};
recorder.onstop = () => {
const blob = new Blob(chunks, { type: 'video/webm' });
const url = URL.createObjectURL(blob);
// 다운로드 링크 생성 등
};
recorder.start();
// ... Canvas에 그리기 작업 수행 ...
recorder.stop();
captureStream()으로 얻은 스트림을 MediaRecorder에 넣기만 하면 된다. Canvas에 무엇을 그리든 — 비디오 위에 텍스트를 오버레이하든, 여러 이미지를 합성하든 — 최종 렌더링 결과가 그대로 녹화된다.
오디오와 합성
Canvas 스트림은 비디오 트랙만 포함한다. 소리도 함께 녹화하려면 오디오 트랙을 별도로 합성해야 한다.
const canvasStream = canvas.captureStream(30);
// 비디오 요소에서 오디오 트랙 추출
const videoElement = document.querySelector('video');
const audioCtx = new AudioContext();
const source = audioCtx.createMediaElementSource(videoElement);
const dest = audioCtx.createMediaStreamDestination();
source.connect(dest);
source.connect(audioCtx.destination); // 스피커로도 출력
// 비디오 트랙 + 오디오 트랙을 하나의 스트림으로 합성
const combinedStream = new MediaStream([
...canvasStream.getVideoTracks(),
...dest.stream.getAudioTracks()
]);
const recorder = new MediaRecorder(combinedStream);
MediaStream 생성자에 트랙 배열을 넣어서 여러 소스의 트랙을 하나의 스트림으로 합칠 수 있다. Web Audio API의 createMediaStreamDestination()으로 오디오를 MediaStreamTrack으로 변환하는 게 핵심이다.
WebRTC 실시간 전송
Canvas 렌더링 결과를 다른 사용자에게 실시간으로 전송할 수 있다. 화면 공유의 커스텀 버전이라고 생각하면 된다.
const stream = canvas.captureStream(15);
// RTCPeerConnection에 트랙 추가
const pc = new RTCPeerConnection(config);
stream.getTracks().forEach(track => {
pc.addTrack(track, stream);
});
일반적인 화면 공유(getDisplayMedia)와 다른 점은, Canvas에 원하는 것만 선택적으로 그릴 수 있다는 것이다. 특정 영역만 공유하거나, 여러 소스를 합성해서 전송하거나, 실시간 주석을 추가하는 등의 커스텀 화면 공유가 가능하다.
<video> 태그에서 미리보기
Canvas에 그리고 있는 내용을 별도의 <video> 요소에서 실시간으로 미리보기할 수 있다.
const stream = canvas.captureStream();
const previewVideo = document.querySelector('#preview');
previewVideo.srcObject = stream;
previewVideo.play();
편집 화면에서는 Canvas로 직접 렌더링하고, 미리보기 영역에서는 <video>로 보여주는 UI를 만들 때 유용하다. <video> 태그의 네이티브 컨트롤(재생/일시정지)도 그대로 활용할 수 있다.
Origin-Clean 제약
Canvas에 다른 도메인의 이미지를 그리면 해당 Canvas는 "tainted"(오염된) 상태가 된다. 오염된 Canvas에서 captureStream()을 호출하면 SecurityError가 발생한다.
// 다른 도메인의 이미지를 그리면 Canvas가 오염됨
const img = new Image();
img.src = 'https://other-domain.com/image.png';
img.onload = () => {
ctx.drawImage(img, 0, 0);
// canvas.captureStream() → SecurityError!
};
이 문제를 해결하려면 이미지에 crossOrigin 속성을 설정하고, 서버가 CORS 헤더를 보내야 한다.
const img = new Image();
img.crossOrigin = 'anonymous';
img.src = 'https://other-domain.com/image.png'; // 서버가 CORS 허용해야 함
img.onload = () => {
ctx.drawImage(img, 0, 0);
canvas.captureStream(); // 정상 동작
};
이 제약은 toDataURL(), toBlob(), getImageData()에도 동일하게 적용된다. Canvas에서 픽셀 데이터를 추출하는 모든 API가 같은 보안 정책을 따른다.
성능 고려사항
frameRate 선택
- 녹화 용도: 24~30fps면 충분하다. 60fps는 파일 크기만 커질 뿐 체감 차이가 크지 않다.
- WebRTC 전송: 네트워크 대역폭을 고려해서 15~25fps 정도가 적당하다.
- 간헐적 업데이트:
frameRate를 생략하거나 0으로 설정해서 불필요한 프레임 생성을 방지한다.
렌더링 루프와의 관계
requestAnimationFrame으로 Canvas에 그리면서 동시에 captureStream(30)으로 캡처하면, 브라우저는 rAF 콜백(보통 60fps)과 별개로 30fps에 맞춰 프레임을 추출한다. 두 타이밍이 정확히 일치하지 않을 수 있어서, 수동 모드(frameRate: 0 + requestFrame())로 렌더링 완료 직후에 캡처하는 게 가장 정확하다.
function render() {
// 모든 레이어를 다 그린 후
ctx.drawImage(backgroundVideo, 0, 0);
ctx.drawImage(overlay, 50, 50);
ctx.fillText(timestamp, 10, 20);
// 렌더링이 완료된 시점에 프레임 캡처
track.requestFrame();
requestAnimationFrame(render);
}
메모리
captureStream()이 반환한 스트림을 사용하지 않게 되면 트랙을 반드시 정리해야 한다.
// 스트림 사용 종료 시
stream.getTracks().forEach(track => track.stop());
트랙을 stop()하지 않으면 브라우저가 계속 Canvas를 감시하면서 프레임을 생성한다. 특히 SPA에서 컴포넌트가 언마운트될 때 정리하지 않으면 메모리 누수의 원인이 된다.
브라우저 호환성
| 브라우저 | 지원 버전 |
|---|---|
| Chrome | 51+ |
| Firefox | 43+ |
| Safari | 11+ |
| Edge | 79+ |
Safari는 frameRate 파라미터를 무시하는 경우가 있다. Safari에서 정확한 프레임 레이트가 필요하면 수동 모드(frameRate: 0)로 requestFrame()을 직접 호출하는 방식이 더 안정적이다.
정리
captureStream(frameRate)로 Canvas 렌더링 결과를 MediaStream으로 변환하며, frameRate 생략(변경 감지), 0(수동), 양수(자동) 세 가지 모드가 있다- 반환된 스트림은 MediaRecorder, WebRTC,
<video>srcObject 등 웹캠 스트림과 동일한 곳에 연결할 수 있다 - CORS 미설정 외부 이미지가 Canvas를 오염시키면 captureStream()이 SecurityError를 발생시키므로, crossOrigin 속성과 서버 CORS 헤더가 필요하다
관련 문서
- Canvas API drawImage - Canvas에 비디오/이미지 그리기
- requestAnimationFrame - 렌더링 루프
- MediaRecorder API - 스트림 녹화