본문 바로가기
카테고리 없음

MediaSoup 을 사용해서 SFU방식으로 VideoChat 구현하기(1/2) 이론

by Lizzie Oh 2023. 1. 22.

 

우리 조 프로젝트는 다대다 화상 통신을 기본으로 하는데, 이 부분을 내가 맡게 되었다.

 

처음에는 SocketIO를 사용해서 Mesh 방식(P2P)으로 다대다 화상 통신을 구현했는데, Mesh 방식은 1:1 통신에 가장 적합하고, 최대 3명까지 정도에 적합한 방식이라는 의견이 많아서 최대 5인 까지 통신해야 하는 우리 프로젝트 특성을 고려해 미디어 서버를 두는 방식으로 변경하기로 했다. 

 

Mesh와 MCU, SFU 방식의 가장 큰 차이는 아래 그림으로 대체하겠다. P2P 방식인 Mesh 방식은 참여자가 모든 다른 참여자에게 자신의 자신의 영상을 보내야 하지만 SFU 방식에서는 미디어 서버에 한 번만 보내면 된다. 구조상으로는 MCU가 가장 간결해보이지만 이 경우 미디어 서버의 부하가 커지기 때문에 우리 조는 클라이언트와 서버가 적절히 부담을 나눠가지는 SFU 방식을 채택하기로 했다. 

 

https://speakerdeck.com/mganeko/build-webrtc-mcu-on-browser?slide=15

 

SFU 방식을 사용하기로 할 때 OpenVidu 와 MediaSoup 라는 두 가지 선택지가 있었다. npm-trends에서 OpenVidu 와 mediasoup 을 비교했을 때 mediasoup이 월등히 이용자 수가 높았는데, mediasoup의 경우에는 구글링해보았을때 국내 레퍼런스가 많이 부족해서 도전하는데 좀 망설였다. 우리 반의 경우 다른 조들은 모두 OpenVidu를 사용한다고 해서 우리만 mediasoup을 쓰는 게 조금 불안하기도 했지만, 그래도 한 편으로는 도전해서 성공해보자 라는 마음도 들어서 결국 mediasoup을 선택했다! 

 

 

여러 자료를 찾다가 어떤 외국 분이 영상으로 자세하게 mediasoup 을 설명해주신 영상이 있어서 큰 도움을 받았고, 영어로 인해서 mediasoup을 사용하는데 어려움을 겪는 분들이 계시다면 조금이나마 도움이 되고자..! 해당 영상으로 공부하고 정리한 내용을 공유해볼까 한다! 

https://www.youtube.com/watch?v=DOe7GkQgwPo

mediasoup 이란 ?

▪️ SFU node.js library
 - standalone 서버가 아니다! 

 - 한 peer가 mediasoup 에게 stream을 보내면, mediasoup이 나머지 모든 soup 에게 이를 보내주는 SFU 구조

▪️ Signaling agnostic 
 - 시그널링 프로토콜이 필요 X

 

 

mediasoup의 기본 구조

▪️  mediasoup은 기본적으로 producerconsumer 라는 컨셉에 기반한다.
 -  producer는 Router를 통해 media를 보내는 쪽을 의미하고,
    consumer는 Router를 통해 media를 받는 쪽을 의미한다.
 - 공식 문서 상의 정확한 정의를 확인해보면 아래와 같다.
     producer:  WebRTC transport 를 통해 mediasoup router로(to) 전송되는 오디오 소스 또는 비디오 소스
    consumer: WebRTC transport 를 통해 mediasoup router에서(from)전송되는 remote 오디오 소스 또는 비디오 소스
 * 여기서 중요한 것은 오디오 '또는' 비디오 소스. 영상과 음성을 함께 전송해야 하는 어플리케이션의 경우 audioProducer와 video Producer 가, audioConsumer, videoConsumer가 각각 존재한다) 

 

▪️ transport
 - mediasoup 공식문서에서는 transport 를 아래와 같이 정의하고 있다
   " A transport connects an endpoint with a mediasoup router and enables transmission of media in both directions by  means of ProducerConsumerDataProducer and  DataConsumer instances created on it. "
   즉 Transport는 각 클라이언트를 mediasoup router와 연결해서 producer, consumer 간 미디어 전송을 가능하게 한다.
- producer와 consumer를 만들기 위해서는 transport 를 생성해야 한다. 
 - transport 는 Router로부터 만들어진다. 

 

▪️ Router
 - mediasoup 공식문서에서는 Router를 아래와 같이 정의하고 있다
   "A router enables injection, selection and forwarding of media streams through Transport instances created on it"
    즉 라우터를 사용해서 미디어 스트림을 전달할 수 있고, 이는 router에 생성된 Transport 인스턴스를 통해 가능하다
    쉽게 이해하자면 Router는 ‘방’ 개념으로 사용될 수 있다. (하나의 router가 하나의 화상채팅 방이 되는 개념)
 - Router는 worker 로 부터 생성된다.

 

▪️ worker
 - worker 하나가 여러 개의 Router를 가질 수 있다
 - 단, CPU 코어 하나 당 하나의 worker 만을 handle 할 수 있으므로, worker 수는 CPU 대수 까지로 제한해야 한다.

 

위의 worker, Router, transport, producer, consumer 객체는 mediasoup에서 모두 제공한다! 👍

 

 

내가 구현하려고 하는 다대다 화상 통신은 한명의 스트리머가 다수에게 비디오 스트림을 보내는 구조(0ne/Few-to-Many)가 아닌, 모두가 모두에게 자신의 비디오, 오디오 스트림을 보내고 또 모두에게 스트림을 받아야 하는 구조(Group Video Chat)이다. 따라서 아래의 구조를 가지게 된다. 

 

 

💡 peer가 N명 이라면

  - 필요한 Transport의 수 : N*N

  - 필요한 Producer 수: N

  - 필요한 Consumer 수 : N(N-1)

 

💡 실제로 mediasoup을 이용해서 코드를 짤 때
    minimum port 번호과 maximum 포트 번호를 정해야 하는데,
    이때 이용자 수를 고려해서 이 포트번호의 range를 설정해 줘야 한다. 
    e.g.
     5 명의 이용자가 있는 서비스라고 하면 최소 25개의 포트 범위가 있어야 한다.

 

 

 

 

 

 

mediasoup 에서의 통신 흐름 

조금 복잡하긴한데 그래도 아래 흐름은 꼭 알아둬야 코드를 짜는 게 가능하다 (ㅠㅠ) 처음에는 진짜 무슨 일인가 싶은데 계속 보다보면 어느정도 익숙해진다. 우선 1명의 producer (영상을 송출하는 사람)과 1의 consumer (영상을 받는 사람) 이 존재하는 상황에서의 흐름을 확인해보도록 하자. 실제 video chat의 경우에는 모든 사람이 producer 이자 consumer 이기 때문에 이 흐름이 반대방향으로도 있어야 한다!  (각 사람이 producer이자 consumer 일때 Device는 한 개만 있어도 된다!) 

 

통신이 복잡해지더라도 이 상황 하나를 잘 이해하고 있으면 이를 응용해서 구현할 수 있다! 시작해보자.

 

1명의 producer와 1명의 consumer 가 있는 상황에서의 diagram

아래를 보면 1개의 worker, 1개의 Router(🔷), 4개의 Transport(서버쪽 2개 🟥  🟩 + 클라이언트쪽 2개 💻  💻  ) 가 존재한다. 
producer / consumer 관점으로 본다면 producer의 클라이언트쪽 transport와 서버쪽 transport가 존재하고, consumer의 클라이언트쪽 transport와 서버쪽 transport가 존재하게 되는 것이다. 이 4개의 transport가 하는 역할은 각각 다르다! 

 

4개의 transport는 LP, RP, LC, RC 라는 용어로 구분되는데, Local Producer, Remote Producer, Local Consumer, Remote Consumer 를 의미한다. Local은 클라이언트측을, Remote는 서버 측을 의미.

 

mediasoup 에서는 producer의 클라이언트 측 transport (LP) 를 SEND Transport 라고 하고, consumer 의 클라이언트 측 transport(LC)를 RECEIVE Transport 라고 한다. 이제 이 용어들을 가지고 실제로 통신이 이루어지는 흐름을 살펴보려고 한다. 아래에서 사용하는 '클라이언트' 란 mediasoup-client 를 사용하는 클라이언트측 js 코드를 의미하고, '서버' 란 mediasoup 라이브러리를 사용하는 서버 측 js 코드를 의미한다! 

 

[1] 각 클라이언트는 Device 객체를 생성하고, 라우터로부터 rtpCapabilities를 받아 load 메서드 실행

 * Device 객체 : 클라이언트 측에서 라우터와 연결하고 데이터를 주고 받는 엔드포인트 역할을 하는 객체. javascript 클라이언트 측에서의 진입점이 된다. new Device() 생성자로 생성된다. 
 * router.rtpCapabilities : mediasoup router 객체의 프로퍼티로서 mediasoup 또는 endpoint가 받을 수 있는 미디어 수준을 결정한다.
  *device.load() : 인자로 router.rtpCapabilities를 받아서 device를 로드한다. 이를 통해 device는 허용되는 미디어 코덱 정보 및 다른 설정 정보들을 알 수 있게 된다. 

 

[2] 서버사이드 webRTCTransport 를 producer, consumer 쪽으로 하나씩 생성해야 하는데, 이를 위해서는 listenIPs나 enableUdp 등등의 options 정보가 필요 함

 

[3] producer 쪽, consumer 쪽 각각의 webRTCTransport로부터 일부 parameter들(trannsportId, iceparameters, icecandidates, dtlsParameters)을 받아서 Client로 전송 → 각 클라이언트는 이 정보를 사용해서 SEND Transport와 RECV Transport를 생성한다

- producer 클라이언트는 device객체의 createSendTransport() 메서드를 호출하여 SEND Transport를 생성한다.
  mediasoup 공식문서에 따르면 device.createSendTransport(options) 호출은 미디어를 전송하기 위한 WebRTC transport를 생성한다. (Creates a new WebRTC transport to send media. The transport must be previously created in the mediasoup router via router.createWebRtcTransport())

 

[4] producer 클라이언트가 이 SEND Transport에서 produce 메서드를 호출하는 경우에 인자로 encodings, codecOptions 와 같은 파라미터들을 넣어주고, 이 메서드는 producer 를 반환한다.
→ produce 메서드의 호출은 SEND Transport의 connect와 produce 라는 이벤트를 발생시키는데 이때 connect 이벤트는 dtlsParametes를 리턴하고, 이는 서버로 보내져서 producer 쪽 서버 transport의 connect 메서드를 호출할 때 그 인자가 됨 (connect(dtlsParameters) )

 

 

[5] produce 이벤트는 kind와 rtpParameters 변수와, callback 메서드와 errback 메서드를 리턴한다.
→ kind와 rtpParameters 변수들은 서버로 보내져서 producer 쪽 서버 transport의 produce 메서드를 호출할 때 그 인자가 됨 (produce(kind, dtlsParameters) )
→ 이는 server side producer를 반환한다!
→ 이 생성된 producer의 id 즉, producer id는 클라이언트로 다시 보내지는데 이때 이 callback 함수를 통해서 SEND transport에 producer id 및 다른 파라미터들을 받았다는 것을 알려준다.
→ 이제 producer는 미디어를 보내기 시작한다

 

 

[6] Consumer는 다바이스로부터 rtpCapabilities를 뽑아서 서버로 던지는데, 이 rtpCapabilties와 producer id가 합쳐져서 라우터가 consume 할 수 있는지를 확인한다. (이는 rtpCapabilities를 보낸 consumer가 producer id를 보낸 producer의 미디어를 소비할 수 있는지 체크하는 과정)
→ if yes, consumer 쪽 server-side transport의 consume 메서드를 호출하고, 이는 server-side consumer 를 반환한다! 이 consumer로 부터 consumerid, producerid, kind, rtpParameter와 같은 파라미터들을 추출할 수 있고, 이는 클라이언트로 보내진다.

 

 

[7] consumer 클라이언트에서 RECV 트랜스포트의 consume 메서드를 호출하고 인자로 이전 단계에서 받은 파라미터들을 사용해서 client-side consumer 를 반환받고, 이는 RECV 트랜스포트의 connect 이벤트를 발생시킨다.
→ 이 connnect는 dtlsParameters를 반환하는데, 이는 서버로 전송되어 consumer 쪽 serverside transport의 connect 이벤트를 호출하여
→ 드디어 consumer도 미디어를 받을 수 있게 된다!

 

[8] Producer 클라이언트는 SEND transport(LP)의 메서드인 produce 메서드를 호출하는데, 이는 connect라는 이벤트와 produce 라는 이벤트를 발생시킨다. connect 이벤트는 dtlsParameters를 반환하는데, 이는 서버로 보내서 server-side transport(RP)의 connect 메서드를 호출한다. 이때 이 connect 메서드의 인자로 dtlsParameters 사용 e.g. connect(dtlsParameters))
produce 이벤트는 parameters, callback, errback을 반환하는데, 이 또한 서버로 보내서 server-side transport(RP)의 produce 메서드를 호출한다. 이때 이 produce 메서드의 인자로 parameter의 일부 정보들을 사용한다

→ 여기까지 완료가 되면 server-side producer가 생성되고🥳, 이 producer의 producer id를 양쪽 클라이언트로 보낸다.

 

[9] producer id를 받은 producer 클라이언트는 8번에서 반환받은 callback을 사용해서 producer id를 SEND transport(LP) 로 보낸다. producer id를 받은 consumer 클라이언트는 mediasoup 서버에 요청을 보내서 Router로부터 server-side webRTC transport(RC) 생성을 요청한다. 

→ medasoup 서버는 Router를 사용해서 webRTC transport(RC) 를 만들고 이 webRTC transport의 RC parameter를 클라이언트로 보낸다.

 

[10] Consumer 클라이언트는 12번에서 받은 parameter와 2번에서 생성한 Device 를 가지고 client-side transport 인 RECV transport(LC)를 생성한다. Consumer 클라이언트는 Device의 rtpCapabilities를 추출해서 producer id 와 함께 서버로 보낸다.
 서버는 이 디바이스의 rtpCapabilites를 받고나서 Router가 consume 할 수 있는지를 체크 (consumer가 실제로 producer가 보내는 내용들을 잘 읽을 수 있는지를 체크하는 것)
→ 만약 consume 할 수 있다면 rtpCapabilities와 producer id를 인자로 넣어 webRTC transport(RC) 의consume 메서드를 호출

 15번까지 완료가 되면 server-side consumer 🥳,와 여러가지 파라미터들이 생성된다. 이 consumer의 consumer id와 파라미터들(kind, rptParameters)을 Consumer 클라이언트로 보낸다.

 

[11] Consumer 클라이언트는 Producer 클라이언트는 RECV transport(LC)의 메서드인 consume 메서드를 호출하는데, 이는 connect라는 이벤트를 발생시킨다.
 connect 이벤트는 dtlsParameters를 반환하는데, 이는 서버로 보내서 server-side transport(RC)의 connect 메서드를 호출한다. 이때 이 connect 메서드의 인자로 dtlsParameters 사용 e.g. connect(dtlsParameters))

 이제 Producer → Consumer 로 미디어 스트리밍이 시작된다!!!

 


커넥션 종료 상황

mediasoup은 연결 종료 상황을 자동으로 골라주지 않으므로, websocket을 통해 어플리케이션에서 잡아내야 한다 → disconnect 이벤트를 잡아내면 close 메서드를 호출해줘야 한다

  1. producer가 연결 종료 → producer transport (RP)의 close메서드를 호출하고, producer 자체의 close 메서드도 호출해줘야 한다.
    → 이후에 자동으로 consumer side에 이벤트를 발생시켜서 consumer 쪽은 자동으로 close 호출된다.
  2. consumer가 연결 종료 → consumer transport (RC)의 close메서드를 호출하고, consumer 자체의 close 메서드도 호출해줘야 한다.
반응형

댓글