만들리에/오락실 (스트리밍 게임)

추억의 오락실 (9/11) - 오락기와 WebRTC

gamz 2021. 2. 8. 21:22

왜 WebRTC 인가?

아무래도 게임이다보니 유저의 입력과 그것이 반영된 화면의 갱신 사이에 딜레이가 있으면 원활한 플레이를 할 수가 없다. 롤할때 젤 많이 하는 소리가 가족분들 안부 묻는거고 그 다음이 아마 핑(ping)계일거다. 그래서 인코딩한 패킷을 가장 빨리 전달할 수 있는 프로토콜을 골라야한다.

 

오락기의 라이브스트리밍을 위한 프로토콜을 고르기 위해 대표적으로 언급되는 RTMP, HLS, WebRTC를 비교해봤다. 가장 중요한 요소는 얼마나 Low-latency 인지 얼마나 Browser에서 잘 지원되는지였다. 

요소

RTMP

HLS

WebRTC

Browser Support

Flash 기술이 들어간 Plugin 이 필요함

표준은 따로 있지만(MPEG-DASH) 거의 De-facto 처럼 많이 쓰임

별도의 Plugin 없이 (모던) 브라우저에서 지원

Latency

수초(2s~)의 Latency 있음

보통 6s~ 정도의 Segment 정도로 나뉘는 만큼 지연이 있음

CMAF를 이용한 Low-latency 방식이 있는 것 같지만 내가 감당이 어려워 보임 

태생이 실시간 커뮤니케이션을 목표로 Sub-second latency 라고 함

Codec

H.264

AAC

H.264, H.265

AAC

VP8, VP9, H.264

Opus

 

WebRTC는 이제 웹표준이기도 하고 RTMP/HLS 처럼 인코딩된 패킷의 Segment를 모아서 전송하는 방식이 아닌 실시간으로 바로바로 전송하는 방식(이 부분은 제가 잘 못 알고 있을 수 있으니 컨펌 필요)이라 다른 프로토콜에 비해 Latency가 매우 낮다. 그리하여 WebRTC를 이용해 오락기의 라이브스트리밍을 하기로 결정했다.

 

참고

Low-latency HLS

 

WebRTC 시그널링(Signaling)

이제 스트리밍 프로토콜로 WebRTC를 선택했으니 이에 대해서 조금 알아보자. 특히, WebRTC 동작의 핵심이라고 볼 수 있는 시그널링에 대해서 조금 정리해보고자 한다.

 

ICE 프로토콜

WebRTC의 특징으로 실시간 통신을 지향한다는 점도 언급했지만 또 다른 주요한 특징으로는 P2P를 지원한다는 것도 있다. 그럼 P2P란 무엇일까. 말그대로 하나의 Peer와 다른 한쪽의 Peer가 중간 참여자가 없이 직접 통신한다는 의미로 비디오/오디오/데이터를 서로서로 직접 전송한다는 말이다.

 

이렇게 직접 통신을 하기 위해서는 서로의 종단(Endpoint) IP 경로를 정확히 알아야하는데 많은 케이스에 클라이언트들(Peer들)은 NAT(공유기 포함) 뒤에 숨어 있기 때문에 부가적인 작업(가령, 포트포워딩)을 해주지 않고서는 연결 자체가 쉽지 않다. 그래서 이런 네트워크 환경에서 P2P 연결을 위해 홀펀칭(Hole-Punching) 기법같은 것들이 존재한다.

 

WebRTC 에서도 이런 연결 이슈들을 위해 ICE 프로토콜을 활용한다. 기본 아이디어는 이런식이다. 각각의 Peer는 Public IP일 수도 있고 NAT 뒤에 있을 수도 있고 NAT가 여러 계층일 수도 있고 두 Peer가 같은 NAT 안에 있을 수도 있는 등 다양한 환경에 있을 수 있다. 그래서 각 Peer가 외부에서 어떻게하면 자신에게 연결을 할 수 있을지의 다양한 시도를 인터넷에 공개된 누군가(STUN)에게 부탁한다. 그렇게 추려진 연결가능한 조합을 각 Peer가 서로 공유할 수 있다면 직접 연결이 가능한 것이다. 하지만 이렇게 해도 도저히 연결가능한 조합을 못 찾아낸다면 그때는 데이터를 Relay 해줄 누군가(TURN)가 필요하게 된다.   

 

SDP 프로토콜

위의 ICE 프로토콜을 통해 얻어낸 연결 정보로 직접 연결하든 TURN서버(Relay)를 통해 연결하든 Peer들이 서로 잘 연결되었다면, 이제 연결된 두 Peer가 주고받을 데이터의 포맷에 대해서 합의하는 과정이 필요하다. 이걸 위한 프로토콜이 SDP 이다.

 

가령, 두 Peer(A, B)가 있다고 해보자. Peer-A는 비디오 코덱 VP8/H.264를 모두 지원하고 Peer-B는 H.264만 지원한다고 했을때 Peer-A가 VP8로 인코딩한 데이터를 보내면 VP8은 볼 수 없기 때문에 이럴때는 H.264를 선택하는게 합리적일 것이다. 이렇게 서로가 "어디까지 알아보고 오셨는지" 등을 주고 받으며 현 세션에 가장 적절한 데이터 포맷을 결정한다.

 

하지만 이게 프로토콜이라고해서 오해하면 안될 것이 SDP는 Session Description 에 대한 포맷을 정의하는 프로토콜이지 전송하는 방식 등을 정의하는 프로토콜은 아니다. 그래서 Peer 들이 Session Description을 서로 주고 받는 방식은 RPC를 하든, HTTP Plain을 하든, 웹소캣을 쓰든 알아서 하면 된다. (오락실에서는 어떻게 했는지는 다음 챕터에서 설명)

 

시퀀스 다이어그램

이제 ICE / SDP 과정을 포함한 시그널링이 어떻게 동작하는지 구체적인 Call-flow로 보자.  

 

 

좀 정신 없어 보이지만 하나씩 살펴보자.

  • 일단 크게 SDP 과정과 ICE 프로토콜 과정으로 나눠진다.

  • SDP

    • 일단 한쪽의 Peer(A)가 createOffer()를 통해 offer description을 생성하고 시그널링서버를 통해 상대 Peer(B)로 전달한다. (전달에 대한 방식은 프로토콜에서 제한하지 않는다.) 그와 동시에 setLocalDescription()으로 자신의 description을 설정한다.

    • 상대 Peer(B)는 offer description을 setRemoteDescription()으로 설정하고 자신의 description을 createAnswer()를 통해 생성하여 전달한다. 그리고 마찬가지로 본인의 description을 setLocalDescription()을 통해 설정한다.

    • Peer(A)도 Peer(B)의 answer description을 전달받고 setRemoteDescription()을 통해 설정한다. 이로써 Peer(A)와 Peer(B)는 서로의 description을 주고받았다.

    • setLocalDescription()와 setRemoteDescription()의 순서는 따로 제약은 없다. 하지만 setLocalDescription()을 수행한 후 부터 브라우저나 라이브러리에서는 ICE candidates을 수집하는 프로세스가 시작된다는 점을 기억하자. (참고)

  • ICE

    • 위의 SDP 과정 중에 setLocalDescription()이 호출된 후에는 ICE candidates을 수집하기 시작한다. RTCPeerConnection을 생성할때 넘겨주는 STUN 서버를 이용해 수집한다.

    • 그럼 onicecandidate() 이벤트 핸들러를 통해 수집된 ICE candidates을 받을 수 있고 우리는 이 정보를 서로의 Remote Peer에 시그널링서버를 통해 전달하면 된다.

    • 각 Peer는 이렇게 전달받은 상대의 ICE candidates를 addIceCandidate()를 통해 설정한다. 그 후에는 서로의 연결 시도를 하게 되며 oniceconnectionstatechange() 이벤트 핸들러를 통해 그 연결 상태를 알 수 있게 된다.  

 

오락기 시그널링 처리

 

오락실에서의 시그널링은 그 위에서 설명한 WebRTC의 동작 순서랑 동일하다. SDP 과정을 통해 서로의 description을 주고받아 설정하고 ICE 과정을 통해 서로의 접속 가능한 연결 정보(ICE Candidates)를 주고 받는다. 그런데 조금 특이사항이 있다면,

  • SDP시에 프론트의 요청-응답이 블럭킹(Blocking)이다. 프론트에서 createOffer로 생성된 값을 아줌마 서버로 보내고 이는 AMQP를 통해 백엔드의 오락기로 전달된다. 오락기는 이렇게 전달된 offer description을 setRemoteDescription()을 통해 세팅하고 createAnswer()로 만들어진 description을 다시 AMQP를 이용해 아줌마로 전달하고 아줌마는 프론트의 요청에 대해 응답으로 오락기의 description을 전달한다. 

  • ICE시에는 프론트의 ICE Candidates 전달은 오락기로 바로 전달되고 오락기의 ICE Candidates은 아줌마를 통해 일단 DB에 저장된 후에 프론트의 Polling을 통해 전달된다. Peer가 직접 만드는 데이터를 공유하는 SDP와 다르게 ICE의 경우에는 STUN 같은 외부 서버에 의존하는 부분이 있어서 비동기로 풀었다.

 

이슈들

브라우저 호환성 문제

WebRTC가 웹 표준이라고는 하지만 아직까지는 각 브라우저 벤더(엔진) 마다 구현 방식이 갭이 있어보인다. 가령, Webkit(사파리 혹은 모바일 크롬/파폭) 기반 브라우저에서 SDP 값이 제대로 안올라온다거나 하는 이슈들이 있었다. 이런 브라우저 구현마다 다른 부분들을 채워주기 위해 adapter.js 를 이용(로드만 해주면 됨)하면 어느정도 표준적인 동작을 기대할 수 있다. (표준까지 오기 위한 노력이 느껴진다.)

 

P2P로 직접 연결이 어려운 상황일때

ICE 과정을 통해서도 종종 P2P 연결은 실패한다. 이럴때는 보통 각 Peer가 모두 접근 가능한 네트워크에 중계기를 두고 패킷을 Relay하는 방식으로 해결하는데 WebRTC에서는 이를 TURN이라는 서버로 제공한다. 가장 많이 사용하는 오픈소스 TURN 서버 중에 하나는 coturn이다. README에 있는 것 처럼 많은 스펙/기능을 지원하는데 특히 괜찮았던 건 STUN 서버 역할도 한다는 것과 인증도 잘 지원한다는 것이다.