Skip to content

Canvas Api를 사용한 방송 송출 화면 구성

Sunny edited this page Dec 1, 2024 · 1 revision

왜 굳이 비디오 트랙을 하나로 합쳐요?

우리 서비스에서는 다음과 같은 기능들이 포함되어 있다.

  1. 화상 캠과 화면 공유 on/off 기능
  2. 방송 송출을 하는 동안 원하는 부분 녹화

이 기능들이 있다면 화상 캠과 화면 공유를 둘 다 켠 상태가 있을 수 있다. 이런 상황에서 화상 캠과 화면 공유 비디오 트랙을 별도로 관리하면 2가지 어려움이 있을 것이라고 예상했다.

  1. WebRTC로 라이브 스트리밍을 구현하여 OBS와 같은 송출 프로그램을 사용하지 않기 때문에 비디오 배치를 실시간으로 변경하기 어려움
  2. 화상 캠과 화면 공유 비디오 트랙을 분리 전송하면 서버에서 녹화 시 비디오를 적절히 배치하여 저장해야 하는 어려움

그렇기에 프론트엔드에서 합쳐서 하나의 비디오 트랙으로 보내주자는 결론을 내렸다.

Canvas API를 이용해 한 화면에 2개의 비디오를 띄우기

비디오를 합치는 방법을 생각해봤는데, Canvas API를 사용해서 한 화면에 그려보는 것이 어떠냐는 팀원의 제안을 들었다. 찾아보니 Canvas API를 사용하면 비디오를 원하는 대로 배치해서 보여줄 수 있고, 해당 화면을 캡처해서 미디어 스트림으로 만들 수 있다는 것을 알았다.

image

그래서 위 사진과 같이 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];

이렇게 구한 비디오 트랙을 송출해주면 된다!

참고 자료

👥 팀 강점

🧑‍💻 개발 일지

📌 ALL

📌 FE

📌 BE

💥 트러블 슈팅

📌 FE

📌 BE

🤔 고민

📚 학습 정리

📌 김광현

📌 백지연

📌 전희선

📌 한승헌

🤝 회의록

🗒️ 데일리 스크럼

💬 팀 회고


👨‍👩‍👧‍👦 소개

🌱 문화

🔨 기술 스택

⚙️ 서비스 아키텍쳐

🚧 CI/CD

🌊 Flow

💭 6주를 보내면서

Clone this wiki locally