-
Notifications
You must be signed in to change notification settings - Fork 1
Canvas Api를 사용한 방송 송출 화면 구성
우리 서비스에서는 다음과 같은 기능들이 포함되어 있다.
- 화상 캠과 화면 공유 on/off 기능
- 방송 송출을 하는 동안 원하는 부분 녹화
이 기능들이 있다면 화상 캠과 화면 공유를 둘 다 켠 상태가 있을 수 있다. 이런 상황에서 화상 캠과 화면 공유 비디오 트랙을 별도로 관리하면 2가지 어려움이 있을 것이라고 예상했다.
- WebRTC로 라이브 스트리밍을 구현하여 OBS와 같은 송출 프로그램을 사용하지 않기 때문에 비디오 배치를 실시간으로 변경하기 어려움
- 화상 캠과 화면 공유 비디오 트랙을 분리 전송하면 서버에서 녹화 시 비디오를 적절히 배치하여 저장해야 하는 어려움
그렇기에 프론트엔드에서 합쳐서 하나의 비디오 트랙으로 보내주자는 결론을 내렸다.
비디오를 합치는 방법을 생각해봤는데, Canvas API를 사용해서 한 화면에 그려보는 것이 어떠냐는 팀원의 제안을 들었다. 찾아보니 Canvas API를 사용하면 비디오를 원하는 대로 배치해서 보여줄 수 있고, 해당 화면을 캡처해서 미디어 스트림으로 만들 수 있다는 것을 알았다.
그래서 위 사진과 같이 2개의 비디오를 적절히 배치하고 해당 화면을 캡처한 미디어 스트림의 비디오 트랙을 송출하기로 결정했다.
이제부터 비디오는 무조건 캔버스를 캡처한 스트림을 보내게 된다.
그래서 다음과 같이 화면 공유와 화상캠의 on/off 상태에 따라 비디오 화면을 다르게 그리도록 구현했다.
const draw = () => {
context.clearRect(0, 0, canvas.width, canvas.height);
if (isScreenSharing && screenShareRef.current) {
const screenVideo = screenShareRef.current;
// 화면 공유 on
// 화면 비율 계산 및 적용
const screenRatio = screenVideo.videoWidth / screenVideo.videoHeight;
const canvasRatio = canvas.width / canvas.height;
const draw = { width: canvas.width, height: canvas.height, x: 0, y: 0 };
if (screenRatio > canvasRatio) {
// 화면이 더 넓은 경우
draw.height = canvas.width / screenRatio;
draw.y = (canvas.height - draw.height) / 2;
} else {
// 화면이 더 좁은 경우
draw.width = canvas.height * screenRatio;
console.log('width', canvas.height / screenRatio);
draw.x = (canvas.width - draw.width) / 2;
}
// 화면 공유 그리기
context.fillStyle = '#000000';
context.fillRect(0, 0, canvas.width, canvas.height);
context.drawImage(screenVideo, draw.x, draw.y, draw.width, draw.height);
// 캠 on
if (isVideoEnabled && videoRef.current) {
const pipWidth = canvas.width / 4;
const pipHeight = canvas.height / 4;
const pipX = canvas.width - pipWidth;
const pipY = canvas.height - pipHeight;
context.drawImage(videoRef.current, pipX, pipY, pipWidth, pipHeight);
}
} else if (isVideoEnabled && videoRef.current) {
// 화면 공유 off / 캠 on
context.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
} else {
// 화면 공유 off / 캠 off
context.fillStyle = '#000000';
context.fillRect(0, 0, canvas.width, canvas.height);
}
requestAnimationFrame(draw);
};
이 draw
함수의 끝에 보면 requestAnimationFrame
함수가 draw
함수 내부에서 재귀호출하고 있는 것을 볼 수 있다. 브라우저는 프레임을 출력할 때마다 rAF(requestAnimationFrame)에 등록된 콜백 함수(여기서는 draw
)를 비동기로 호출하여 비디오를 부드럽게 출력해주게 된다.
이제 captureStream
메서드로 이렇게 비디오가 그려지고 있는 canvas를 캡처하면 우리가 원하는 모양대로 그려진 하나의 비디오 트랙이 포함된 미디어 스트림을 얻을 수 있게 된다.
// 캔버스의 비디오 트랙
canvas.captureStream(30).getVideoTracks()[0];
이렇게 구한 비디오 트랙을 송출해주면 된다!
- Mediasoup 포트 매핑 문제
- swagger 같은 응답 코드에 다양한 응답 보여주기
- Sudo가 계속 비밀번호를 요청함
- Docker 이미지가 너무 크다
- Git action에서 도커 이미지 빌드 시간을 단축시켜보자
- Docker compose를 이용해서 메모리 사용률을 줄여보자
- 방송 녹화 시 CPU 과부하 문제를 해결해보자
- Release 브랜치? 너 필요해?
- 로딩이 너무 짧아…!
- NestJS ORM으로 무엇을 사용해야 할까?
- WebRTC를 이용한 1:N 스트리밍 서비스에서 시그널링 서버가 필요할까?
- 실시간 채팅 구현: 인메모리 방식을 선택한 이유
- MySQL 아키텍처 개선: DB 의존성 분리와 서버 역할 명확화
- 브라우저 창이 최소화되면 비디오 송출이 안된다…!
- Mediasoup 기본 개념
- DLTS와 Signaling
- Tell, Don't Ask (TDA) 원칙이란
- VPC(Virtual Private Cloud) 학습 정리
- 순환참조: A 서비스 ‐ B 서비스 vs. A 서비스 ‐ B 레포지토리
- Dto 메서드 전략
- WebRTC란?
- 자바스크립트 패키지 매니저(npm, yarn, pnpm)
- shadcn/ui을 이용해 UI 개발 생산성 높이기
- React 이벤트 핸들러 네이밍(on vs handle)
- React-router-dom의 createBrowserRouter을 사용해보기
- fetch vs axios