-
Notifications
You must be signed in to change notification settings - Fork 0
WebStudio 개발기
우리 팀은 영상을 주제로 프로젝트를 진행하게 되었고 VOD, 숏폼, 실시간 화상회의 등 영상으로 할 수 있는 여러 선택지들이 있었지만 그 중에서도 실시간 스트리밍 서비스를 선택하게 되었다.
부스트캠프 이전 기수에서도, 올해 기수에서도 실시간 스트리밍 서비스를 주제로 선택한 팀은 몇몇 있었는데, 다른 팀들과 가장 차별화되고 우리 팀만의 특징적인 기능이 웹 스튜디오이다.
대부분의 다른 스트리밍 플랫폼 (치지직, 트위치, 숲 등) 에서는 OBS
나 PRISM Studio
와 같은 외부 송출 소프트웨어로 방송을 송출할 수 있다.
👆치지직, OBS
그리고 유튜브는 브라우저에서의 방송 송출을 지원하지만 화면공유나 웹캠의 드래그나 리사이징 등 상호작용을 지원하지 않고 사진, 텍스트 넣기, 그리기 등 외부 소프트웨어의 기능들도 지원하지 못한다.
👆유튜브 스튜디오
그래서 외부 소프트웨어를 사용하지 않고 브라우저 안에서 다양한 기능들을 지원하며 쉽게 방송을 시작할 수 있게 하자! 라는 생각으로 웹 스튜디오를 이번 프로젝트의 주요 기능으로 선택했다.
맨 처음엔 그냥 단순하게 화면공유나 웹캠 위에 캔버스를 두고 그린다음 WebRTC
를 이용해서 송출하면 되겠지? 라고 생각했지만 몇가지 브레이크 포인트가 걸렸다.
- WebRTC
WebRTC는 P2P 연결을 기반으로 설계되어서 다수의 시청자들에게 동시에 스트리밍을 해야하는 경우에는 여러 문제가 발생한다.
- Canvas Layer
방송화면, 웹캠, 텍스트, 사진, 선 등이 들어가는 캔버스 등 여러 개의 캔버스를 두고 사용하다 보면 여러 문제가 발생할 수 있을 것 같다.
레이어가 겹치게 되어서 마우스 이벤트가 의도한 대로 동작하지 않는다거나 리사이즈나 드래그 시 캔버스의 위치를 동기화하지 않으면 레이어들이 어긋나게 될 것이다.
또 캔버스 렌더링은 비용이 많이 드는 작업인데 이걸 어떻게 효율적으로 처리할 수 있을까
WebRTC는 기본적으로 P2P 연결을 기반으로 설계되었다. 이는 소규모 화상 회의나 1대1 통신에는 지연이 매우 적고 효율적이지만 다수의 시청자에게 동시에 스트리밍을 해야 하는 경우에는 각각의 시청자와 개별적인 연결을 유지해야 하므로 스트리머의 네트워크 대역폭과 컴퓨터 자원이 급격히 소진된다.
또한 WebRTC 미디어 서버에서는 각 시청자마다 별도의 미디어 스트림을 생성하고 전송해야하므로 스트리머의 업로드 대역폭이 시청자 수에 비례하여 증가한다. 만약 스트림이 5Mbps 라면 1000명의 시청자에게 전송하기 위해서는 5Gbps의 업로드 대역폭이 필요하게 된다.
다음으로 WebRTC는 신뢰성 있는 실시간 통신을 위해 SRTP를 사용한다. 이 프로토콜은 패킷 손실을 최소화하고 지연을 줄이는데 초점이 맞춰져 있어 각 연결마다 추가적인 오버헤드가 발생하는데 대규모 스트리밍에서는 이러한 오버헤드가 누적되어 전체 시스템의 성능을 저하시킨다.
따라서 WebRTC를 P2P 연결로 사용하지 않고 RTMP와 같이 스트리밍 송출용으로만 사용하고 HLS로 변환하여 배포하는 방식을 선택했다. 스트리머는 WebRTC를 통해 미디어 서버와 P2P 연결을 맺으므로 송출자의 자원 소비가 매우 적고 P2P로 연결하기 위해 필요한 STUN서버, TURN서버 등이 필요하지 않아 간단하다.
기존 미디어서버는 WebRTC를 수신하지 못해서 인제스트 서버를 추가하여 WebRTC를 수신한 후 RTMP로 변환하여 미디어서버로 전송하여 처리했다.
먼저 기존 WebRTC 연결 아키텍처는 다음과 같다.
생소한 용어들에 대해 설명하면,
-
SDP(Session Description Protocol) :
-
미디어 세션에 대한
설명서
-
내가 이런 방식으로 스트리밍 할거야~
-
포함되는 정보 :
- 사용할 미디어 코덱 (H.264, VP8 등)
- 해상도, 프레임레이트
- 대역폭 정보
- 네트워크 정보 (IP, 포트)
- 보안 매개변수
-
예시
v=0 o=- 7614219274584779017 2 IN IP4 127.0.0.1 s=- t=0 0 a=group:BUNDLE 0 a=msid-semantic: WMS m=video 9 UDP/TLS/RTP/SAVPF 96 97 c=IN IP4 0.0.0.0 a=rtcp:9 IN IP4 0.0.0.0 a=ice-ufrag:1/MvHwjAyVf27aLYN a=ice-pwd:3dBU7cFOBl120v33cynDvN a=fingerprint:sha-256 D2:B9:31:8F:DF:24:D8:0E:ED:D2:EF:25:9E:AF:6F:B8:34:AE:53:9C:E6:F3:8F:F2:64:15:FA:E8:7F:53:2D:38 a=setup:actpass a=mid:0 a=sendonly a=rtcp-mux a=rtcp-rsize a=rtpmap:96 VP8/90000 a=rtcp-fb:96 nack a=rtcp-fb:96 nack pli a=rtcp-fb:96 goog-remb
-
-
ICE(Interactive Connectivity Establishment) :
-
최적의 네트워크 경로를 찾는 과정
-
여기로 연결하면 돼~
-
NAT 통과 문제를 해결하기 위한 방법 찾음
-
ICE Candidate 타입 :
1. Host Candidate: 직접 로컬 네트워크 연결 예: "192.168.1.100:45692" 2. Reflexive Candidate (STUN): NAT 뒤에 있을 때의 공인 IP 예: "82.54.23.111:19832" 3. Relay Candidate (TURN): 직접 연결이 안될 때 중계 서버 예: "turn.example.com:3478"
-
그런데 우리 미디어 서버에서는 WHIP(WebRTC HTTP Ingestion Protocol) 방식으로 연결을 수립한다. WHIP 방식은 단순한 HTTP 기반 프로토콜로 단방향 스트리밍에 최적화 되어있고 연결 유지가 필요 없는 stateless 통신 방식이다.
- HTTP 요청으로 단방향 통신
- POST로 초기 SDP 교환
- ICE Candidate는 서버가 내부적으로 처리
- 연결 유지 불필요
캔버스를 어떻게 스트림으로 합칠지 고민했다.
- 하나의 캔버스에 모든 콘텐츠를 순차적으로 그린 후 해당 캔버스를 스트림으로 전송
👍 : 최소한의 스트림 (WebRTC에서 처리해야 할 스트림이 하나라서 관리가 쉬움)
👍 : 구현이 매우 간단
👍 : 네트워크 부하가 가장 낮음
👍 : 레이어 간 동기화 문제가 없음
👎 : 송신측 CPU/GPU 부하가 매우 높음
👎 : 개별 요소의 수정이나 재배치가 어려움
👎 : 모든 컨텐츠가 하나의 캔버스 해상도에 맞춰짐
👎 : 특정 요소만 업데이트하기 어려움
- 각 요소를 독립적인 캔버스에 그리고 각각을 별도의 스트림으로 전송한 후 수신측에서 합성
👍 : 각 요소를 독립적으로 제어
👍 : 각 스트림별로 다른 해상도와 품질 설정
👍 : 송신측 부하 분산
👍 : 동적인 레이아웃 변경 용이
👎 : 매우 높은 네트워크 대역폭
👎 : 수신측에서 높은 CPU/GPU 부하 발생
👎 : 여러 WebRTC 연결 필요
👎 : 스트림 간 동기화 문제 발생 가능
👍 : 각 요소를 독립적으로 관리하면서도 네트워크 효율성 유지
👍 : 개별 캔버스에서 최적화된 처리
👍 : 레이어 관리 용이
👍 : 부분적인 업데이트
👍 : 유지보수하기 좋음
👎 : 송신측에서 2번의 렌더링 과정 필요
👎 : 구현 복잡
👎 : 메모리 사용량이 더 많음
👎 : 최종 합성시 성능 부하
웹 브라우저의 캔버스 렌더링은 일반적인 렌더링과 차이가 있다. 일반적인 HTML 요소들은 브라우저의 렌더링 엔진이 최적화된 방식으로 처리한다. 예를 들어 div나 image 같은 요소들은 브라우저가 GPU를 효율적을 활용하여 렌더링한다.
그러나 캔버스는 픽셀 단위로 직접 그림을 그리는 low-level API이다. 캔버스에 무언가 그릴때마다 CPU가 각 픽셀의 색상값을 계산하고 이를 GPU로 전송하여 화면에 표시하는데 이때 여러가지 비용이 발생한다.
- CPU 계산 비용 : 각 픽셀의 색상값을 결정
- 메모리 사용 비용 : 모든 픽셀 정보를 메모리에 저장해야하는데 1920x1080 해상도라면 각 픽셀당 4바이트 (RGBA)를 곱한 8MB의 메모리 필요
- CPU→GPU 사이의 데이터 전송 비용 : 캔버스의 내용이 변경될 때마다 변경된 픽셀 데이터를 GPU로 전달해야하는데 이 과정이 상당한 대역폭을 소비함
→ 2번은 서버에서 구현해야하므로 불가능. 1번은 매번 전체 캔버스를 다시 그리고 전송해야 하므로 불필요한 오버헤드 발생. 3번은 변경이 필요한 부분만 업데이트 되고 전송 비용은 첫번째 방법과 비슷함. 메모리 사용량이 늘어나지만 현대의 웹 브라우저는 수백MB까지도 무리가 없지만 CPU사용량이 높아지면 프레임드랍, 브라우저 전체 성능 저하, 사용자 입력에 대한 반응 지연, 발열, 배터리 소모량 증가 등 문제 발생. 즉 CPU 최적화를 우선순위에 두어야 함
→ 3번으로 채택
- 여러 설정과 상태들을 넘겨주어서 처리
- 활성화된 버튼에 따라 z-index 다르게 처리
- 방송 시작하기 버튼을 누르면 WHIP 연결 수립 후 캔버스 합성하여 스트림 전송
- 보일러플레이트가 적어서 구현이 간단하고 가독성이 좋아짐
- 상태 업데이트가 빈번하고 여러 컴포넌트가 동시에 상태를 참조하므로 선택적 구독을 통한 리렌더링 방지를 통한 성능 최적화
- 단일 스토어에 모든 상태가 저장되므로 메모리 관리가 단순함