이번에 크위치에 유저의 캠과 마이크 음성을 추가하기로 결정했다.
스트리머 부분은 간단했다.
처음엔 전체화면에 대해 생각하지 않았기 때문에 두개의 video 태그를 생성해서 각 layout에 따라 위치와 크기를 조정하도록 했다.
for (const track of mediaStream.getVideoTracks()) {
const producer = await createProducer({
transport: sendTransport,
producerOptions: {
track,
appData: {
source,
},
},
})
producers.video = producer
}
두개의 Producer를 구분하는 방법은 단순히 producerOptions에 appData에 source를 "display" | "user"
로 구분했다.
appData는 mediasoup Entity에 대해 개발자가 Custom한 데이터를 넣을 수 있는 부분입니다.
docs: producer.appData
문제는 여기서 시작됬다.
두개의 Video 태그를 사용하면 어떤 화면을 전체화면 할지 결정할 수 없다는 걸 간과했다.
따라서 두개의 Video 태그를 한 곳에 묶어 전체화면을 가능하도록 했어야 했다.
const drawCanvas = () => {
...
const draw = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(displayVideo, 0, 0, canvas.width, canvas.height)
ctx.drawImage(
userVideo,
canvas.width - userVideo.videoWidth,
canvas.height - userVideo.videoHeight,
userVideo.videoWidth,
userVideo.videoHeight,
)
requestAnimationFrame(draw)
}
requestAnimationFrame(draw)
}
내 계획은 Canvas 위에 두개의 Video를 layout에 맞게 그리고 requestFullscreen()
을 통해 전체화면을 보여줄 예정이였다.
중요한점은 clearRect를 꼭 해줘야 한다는 점이다. 안그러면 사용하는 메모리가 기하급수적으로 상승한다.
Video 태그를 전체화면 했을 땐, 정지, 음소거, 전체화면 해제 등 컨트롤 패널이 주어졌었는데, Canvas를 전체화면하면 이런 것들이 불가능했다.
기존에 화면에 구현해 놓았던 패널을 사용하면 가능하지만 전체화면에서 ESC를 누르고 사용한다는건 너무 불편해보였다.
다시 Video 태그를 사용할 방법을 알아보다가 Canvas.captureStream를 찾았다.
이는 MediaStream
의 일종인 CanvasCaptureMediaStreamTrack
를 반환하는데, Canvas의 실시간 비디오를 가져올 수 있는 것이였다.
따라서 canvas에서 합친 후 다시 video를 가져오게 하면 된다는 것을 알았다.
const drawCanvas = () => {
if (
!combineVideoRef.current ||
!displayVideoRef.current ||
!userVideoRef.current
) {
return
}
const canvas = document.createElement("canvas")
const ctx = canvas.getContext("2d")
if (!ctx) return
const displayVideo = displayVideoRef.current
const userVideo = userVideoRef.current
canvas.width = displayVideo.videoWidth
canvas.height = displayVideo.videoHeight
ctx.imageSmoothingEnabled = false // 성능을 위해 비활성화
combineVideoRef.current.srcObject = canvas.captureStream(30)
combineVideoRef.current.play().catch((error) => {
console.error("Error playing video:", error)
})
const draw = () => {
...
}
requestAnimationFrame(draw)
}
Canvas를 동적으로 생성으로 생성하고 combineVideoRef
에 연결된 Video 태그에 실시간으로 가져왔다.
여기서 중요한 부분이 두개가 있는데
displayVideo.onloadedmetadata = () => {
if (userVideo.readyState >= 1) {
drawCanvas()
}
}
userVideo.onloadedmetadata = () => {
if (displayVideo.readyState >= 1) {
drawCanvas()
}
}
videoWidth
함수를 사용하는 부분이 보일텐데, 이는 ref의 current가 존재한다고 바로 나오는 부분이 아니다. Video 태그의 metadata가 준비되어야 이를 잘 응답하므로 이를 잘 처리해야 한다. 나는 두개의 Video 태그가 존재하므로 둘 다 준비되었을때 Cavas를 그리도록 했다.combineVideoRef.current.srcObject = canvas.captureStream(0)
두 개의 비디오가 잘 합쳐지며 전체화면까지 문제없이 동작하는 화면을
추가적으로 layout에 대한 업데이트 처리를 하고 성능을 좀 더 테스트 한 뒤에 프로덕션에 반영할 예정이다.
진행사항은 이 이슈에서 확인이 가능하다.