개요
난 네이버 부스트캠프 그룹 프로젝트에서 영상을 주제로 선택했고, 우리 팀은 그중에서도 실시간 스트리밍 서비스를 만들기로 결정했다.
부스트캠프 이전 기수에서도, 올해 기수에서도 실시간 스트리밍 서비스를 주제로 선택한 팀은 몇 있었지만, 다른 팀들과 가장 차별화되고 우리 팀만의 특징적인 기능이 `웹 스튜디오`이다.
이 글에서는 `웹 스튜디오`를 구현하면서 학습하고 고민한 내용, 이를 통해 어떻게 선택해서 구현했는지를 다룬다.
대부분의 다른 스트리밍 플랫폼 (치지직, SOOP 등)에서는 OBS
나 PRISM Studio
와 같은 외부 송출 소프트웨어로 방송을 송출할 수 있다.
그리고 유튜브 스튜디오에서는 브라우저에서 직접적인 방송 송출을 지원하지만 화면공유와 웹캠을 드래그해서 위치를 변경하거나 리사이징 등 상호작용을 지원하지 않고 사진, 텍스트 넣기, 그리기 등의 기능들도 지원하지 않는다.
그래서 외부 소프트웨어를 별도로 설치하지 않고 브라우저 안에서 다양한 기능들을 지원하며 쉽게 방송을 시작할 수 있게 하자! 하는 생각으로 웹 스튜디오를 이번 프로젝트의 주요 기능으로 선택하게 되었다.
WebRTC
처음엔 단순히 canvas 위에 화면공유, 웹캠 등을 그리고 WebRTC를 이용해서 송출하면 되겠지?라고 생각했다.
https://webrtc.github.io/samples/src/content/capture/canvas-pc/
Canvas to peer connection
WebRTC samples Stream from canvas to peer connection Due to autoplay policy the video seems not to be playing. Clicking the left teapot usually resolves this. Click and drag on the canvas element (on the left) to move the teapot. This demo requires Firefox
webrtc.github.io
아키텍처를 설명하기 전에 먼저 WebRTC에 대해서 알아보자.
WebRTC는 별도의 플러그인 없이 실시간 소통(비디오, 오디오)이 가능하도록 하는 기술이다. 기본적으로는 `P2P`(Peer to Peer), 즉 두 단말이 서로 1대 1 통신을 하도록 되어있다. 따라서 대규모 방송 서비스를 구축하거나 컨텐츠 가공이 필요한 경우에는 중앙 서버를 구축할 필요성이 생긴다. 이런 경우 목적에 따라 SFU, MCU의 두 가지 아키텍처를 고려해볼 수 있고 이는 아래에서 자세히 다루도록 한다.
두 기기가 실시간 소통을 하기 위해서는 다음과 같은 사항이 필요하다.
- 미디어 스트림 획득: 카메라, 마이크 등 입력 장치로부터 데이터를 가져옴
- 네트워크 정보 수집: 통신할 상대방의 IP 주소와 포트 정보 파악
- 신호 통신 관리: 오류 처리와 세션 초기화
- 미디어 호환성 확인: 해상도, 코덱 등 서로 호환되는 형식 협의
- 연결 수립: 실제 P2P 연결 구성
- 미디어 스트리밍: 연결된 채널을 통해 실제 데이터를 주고받음
이를 위해서 WebRTC는 API를 제공하는데
- MediaStream API : 사용자의 카메라 혹은 마이크 등 input 기기의 데이터 스트림에 접근한다.
- RTCPeerConnection API: P2P 연결의 핵심 담당
- RTCDataChannel API: 영상/음성 이외의 일반적인 데이터 P2P 통신. (채팅 메시지, 파일 전송 등)
이 API들은 위 사항의 일부만 만족시키고 통신할 peer 간 정보를 교환하는 Signaling 과정이 추가적으로 필요하다.
- Media capabilites : 내 브라우저와 상대 브라우저가 사용 가능한 코덱과 해상도
- Network configuration : 외부에서 보는 내 컴퓨터의 IP 주소와 포트
- Session control message : 통신의 초기화, 종료, 에러 리포트
이 작업은 스트리밍이 시작하기 전에 완료되어야 한다.
미디어 정보 교환 Media capabilities
통신을 시작하기 전에 SDP(Session Description Protocol)를 통해 미디어 정보를 교환한다. SDP는 미디어 세션에 대한 `설명서`로 사용할 코덱, 해상도, 프레임레이트, 대역폭 정보, 네트워크 정보, 보안 매개변수 등을 포함한다. 즉 `내가 이런 방식으로 스트리밍 할 거야~` 하는 정보를 담고 있다. 이는 offer와 answer 방식으로 진행되며 각 피어가 자신의 미디어 처리 능력을 상대방에게 알리는 역할을 한다.
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
네트워크 정보 교환 Network configuration
현대 네트워크 환경에서는 대부분의 기기가 NAT(Network Address Translation) 뒤에 있기 때문에 직접적인 P2P 연결이 쉽지 않다. 이 문제를 해결하기 위해 WebRTC는 ICE(Interactive Connectivity Establishment) 프레임워크를 사용하여 서로의 IP와 포트를 찾는다.
NAT ?
인터넷에서 통신하기 위해 기기마다 고유한 IP가 필요하다. 하지만 전 세계의 모든 기기에 고유한 공인 IP 주소를 부여하기에는 사용 가능한 IPv4 주소의 개수가 부족하다. NAT는 하나의 공인 IP 주소를 여러 대의 기기가 공유할 수 있게 해주는 기술이다. 가정이나 회사의 라우터는 하나의 공인 IP 주소를 부여받고 라우터에 연결된 각 기기는 사설 IP 주소를 받는다. 내부 기기가 인터넷으로 데이터를 보낼 때 NAT는 사설 IP 주소를 공인 IP 주소로 변환하고 어떤 기기가 어떤 통신을 했는지 기록해 둔다. 그리고 외부에서 응답이 오면 NAT는 기록을 참고하여 올바른 내부 기기로 전달한다.
ICE는 다음과 같은 서버들의 도움을 받아 최적의 연결 경로를 찾는다.
- STUN(Session Traversal Utilities for NAT) 서버는 클라이언트의 공인 IP 주소와 포트를 확인하는 역할
- TURN(Traversal Using Relays around NAT) 서버는 직접 연결이 불가능한 경우를 위한 중계 서버 역할
ICE는 여러 종류의 연결 후보(candidate)를 시도하며, 가장 효율적인 연결 방식을 찾아낸다. 호스트 candidate(직접 연결), reflectice candidate(STUN을 통한 연결), relay candidate(TURN을 통한 연결) 등 다양한 옵션을 시도하여 최적의 연결을 구성한다. 간단히 `여기로 연결하면 돼~`를 찾는 과정이다.
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"
그리고 위의 Session control messages 과정은 위 과정들에서 필요한 마이너한 과정들을 채워주고, 시그널링을 마치고 나면 데이터(미디어)는 P2P로 통신하게 된다.
앞서 말한 것처럼, 기본적으로는 P2P (peer to peer), 즉 두 단말이 서로 1대 1 통신을 하도록 되어있다. 따라서 대규모 방송 서비스를 구축하거나 컨텐츠 가공이 필요할 경우에는 중앙 미디어 서버를 구축할 필요성이 생긴다. 이런 경우 목적에 따라 MCU와 SFU의 두 가지 아키텍처를 고려해 볼 수 있다.
WebRTC 통신 아키텍처
P2P
원래의 P2P는 중앙 미디어 서버 없이 종단 간 직접 연결하므로 비용 측면에서 이득이 있다. 다만 peer 수가 증가할수록 개별 기기의 높은 성능을 요구한다. 1:1, 최소한 미디어 교환에 적합하다.
MCU (Multipoint Control Unit)
한쪽 Peer에 서버를 두고, 들어오는 트래픽을 서버에서 믹싱 해서 다시 내보내는 방식이다. 간단히 말하자면 모든 참가자의 영상을 하나로 합쳐서 보내준다. 클라이언트와 네트워크의 부담이 줄어드는 반면, 중앙서버의 컴퓨팅 파워가 많이 요구된다. 낡은 기술이고 서버 운용 비용이 높아, WebRTC와 같은 실시간성 보장이 우선인 서비스인 경우 장점이 상쇄된다.
SFU (Selective Forwarding Unit)
믹싱하지 않고 트래픽을 선별적으로 배분해서 보내주는 방식이다. 개별 영상들을 컴퓨터나 휴대폰이 받아서 화면에 배치한다. 각 peer 연결 할당을 서버가 담당하고 1:N 스트리밍 구조에 적합하다고 한다.
-> ZOOM에서 여러 사용자의 화면이 한 번에 보이는 것
WHIP
그렇다면 이중 어떤 방식을 선택해야 할까?
실시간 스트리밍 서비스에서 방송을 송출하는 것은 스트리머 -> 시청자로의 단방향 스트리밍이고, 대규모 스트리밍 서비스를 위해서 스트리머는 시청자 각각과 WebRTC 연결을 맺어야 하므로 복잡한 시그널링 과정을 필요로 했다. 따라서 사실 위의 방식들 모두 우리의 서비스에는 적합하지 않았다.
다른 개발자들도 비슷한 고민을 했는지 아마존, 구글 등 클라우드 제공 업체들이 WebRTC 기반 스트리밍 서비스를 제공하면서 표준화된 인제스트 프로토콜의 필요성이 대두되었고, Meta가 주도적으로 개발한 WHIP 프로토콜이 2023년 IETF에서 초안 표준으로 발표되었다.
WHIP는 HTTP를 사용하여 WebRTC 연결을 설정한다.
작동 과정은 다음과 같다.
- 스트리머가 미디어 서버의 WHIP 엔드포인트로 HTTP POST 요청
- 요청 본문에는 SDP offer 포함
- Content-Type은 'application/sdp'
- 미디어 서버는 SDP answer로 응답
- 201 Created 응답
- Location 헤더에는 스트림을 관리할 수 있는 URL이 포함
- WebRTC 연결이 수립되고 스트리밍 시작
- 스트리밍을 종료하려면 Location URL로 DELETE 요청
WHIP 프로토콜로 ICE Candidate는 서버가 내부적으로 처리하고, STUN/TURN 서버가 필요하지 않아 시그널링 과정을 단순화하여 쉽게 구현할 수 있었다.
결론적으로 우리 프로젝트는 스트리머와 인제스트 서버가 1대 1 연결을 하여 WebRTC로 미디어를 송출하고, 인제스트 서버는 WebRTC를 전통적인 RTMP로 변환하여 미디어 서버로 전송하고, 미디어 서버는 이를 다시 HLS로 변환하여 시청자들이 방송을 시청할 수 있게 된다.
Canvas 구조에 대한 고민
`웹 스튜디오`는 단순히 고정된 위치의 웹캠과 화면 공유만 송출하는 유튜브 스튜디오와 달리, 외부 송출 소프트웨어(`OBS`나 `PRISM Studio`)처럼 자유로운 웹캠과 화면 공유의 위치 변경과 리사이징, 이미지와 텍스트 삽입, 그리기와 지우기 등 실제 라이브 스트리밍에서 활용되는 기능들을 구현하고자 했다. 따라서 어떤 구조를 가져야 마우스 이벤트가 의도한 대로 동작하면서도 효율적으로 처리할 수 있을지에 대해 고민했다.
1. 캔버스 하나에 모든 레이어를 그려서 하나의 스트림으로 송신
가장 처음에 생각한 방법이다. 하나의 캔버스에 모든 컨텐츠를 순차적으로 그린 후 해당 캔버스를 스트림으로 전송한다. 이 방법은 구현이 가장 간단하고 하나의 캔버스만 전송하면 되므로 네트워크 부하도 낮지만 두 가지 심각한 문제가 발생한다.
첫 번째로, 각 요소가 의도한 대로 동작하지 않는다. 화면 조정 테두리가 다른 요소들에 의해 가려진다거나, 나는 그림이나 텍스트만 지우고 싶은데 이미지도 같이 지워진다거나, 같은 요소인데 어떤 건 위로가고, 어떤건 아래로 가고 해서 마우스 이벤트가 동작하지 않는다거나 하는 등 하나가 해결되면 새로운 문제가 계속해서 발생했다. 이 때문에 각 캔버스의 분리가 필수적이구나라고 생각했다. 두 번째로, 비디오와 웹캠의 경우에는 매 프레임마다 계속해서 다시 그려야 하지만, 다른 요소들은 상호작용 시에만 변경되고 그 이후에는 변경되지 않는다. 그러나 하나의 캔버스에서는 매 프레임마다 비디오와 함께 정적인 요소들도 다시 그려야 해서 불필요한 렌더링이 발생하게 된다.
그런데 여기서 하나의 캔버스에서 매 프레임마다 모든 요소를 다 그리는 거나 여러 캔버스로 분리한 다음 비디오 캔버스를 그리는거나 결국 둘 다 매 프레임마다 캔버스 하나를 그리는 건 똑같은 게 아닌가? 그리고 합성하는데도 추가적으로 비용이 드는 것 아닌가? 하는 의문이 생긴다. 이를 알아보기 위해 먼저 캔버스 렌더링 과정을 살펴보자.
Canvas 렌더링 과정
먼저 캔버스 렌더링과 비교하기 위해, 일반적인 HTML 요소들이 어떻게 렌더링 되는지부터 간단히 살펴보자.
브라우저에는 위의 세 가지 모듈들이 유기적으로 연계되어서 보여주게 된다.
- Rendering Engine (Blink) : DOM과 CSS 파싱
- Javascript Engine (V8) : 작성한 자바스크립트 해석
- Graphics Library (SKIA) : 작성한 HTML, CSS를 이미지로 그리기 위해서 사용되는 라이브러리
HTML 코드를 렌더링 엔진에 넣으면 DOM Tree 구조로 파싱 한다. (어떤 모양을 그릴지 결정)
마찬가지로, CSS 코드 또한 파싱 해서 최종적으로 Layout Tree라는 자료구조 형태를 만들어낸다.
이후 Layout Tree의 각 엘리먼트들이 어떤 위치와 사이즈로 그려질지를 결정하는 Layouting을 하게 되고,
그다음 그리는 순서를 결정하는 Layerization 과정이 진행된다.
즉, 렌더링 엔진은 `어떤 모양`으로, `어느 위치`에 `어느 정도 크기`로, `어떤 순서`로 그릴 지를 결정하게 된다.
만들어진 자료구조에는 브라우저가 어떤 그림을 그릴지에 대한 정보를 담고 있고 그래픽스 라이브러리가 실제로 요소들을 그리게 된다.
그러나 캔버스의 내부를 채우는 건 개발자의 손에 달려있다. 즉 javascript 코드에 따라서 내부 캔버스 내부를 채우게 된다.
렌더링 엔진에서 <script> 태그를 만나게 되고, 렌더링 엔진은 자바스크립트를 해석할 수 없기 때문에 자바스크립트 엔진에 해석을 요청한다. 자바스크립트 엔진에서는 코드를 해석한 다음 다시 렌더링 엔진에게 요청한다. 그리고 렌더링 엔진은 다시 2d 그래픽스 라이브러리인 SKIA에게 넘겨준다. SKIA 라이브러리 내부에서는 CPU에서 각 픽셀의 색상값을 계산하고 GPU로 데이터를 전송한다. 이후 GPU가 받은 데이터를 바탕으로 실제로 화면에 그리게 된다. 이 canvas 렌더링 파이프라인을 따라 단일 캔버스 방식과 각 캔버스를 분리하는 방식을 비교해 보자.
1. Blink 엔진에서 Script 태그 처리
Blink 엔진에 HTML을 파싱 하다가 script 태그를 만나면, 이 javascript 태그를 V8 엔진으로 전달한다. 이 단계에서는 두 방식 모두 단순히 DOM 요소를 생성하는 것이므로 큰 차이가 없다.
2. V8 엔진의 javascript 처리
단일 캔버스 방식은 매 프레임마다 모든 canvas API 호출이 발생하지만, 레이어 분리 방식은 정적 요소(이미지, 텍스트, 그림 등)의 경우에는 초기 한 번만 호출한다.
3. Blink 엔진의 그래픽 명령 처리
Blink 엔진은 Canvas API 호출을 받아서 SKIA가 이해할 수 있는 그래픽 명령으로 변환한다. 단일 캔버스 방식은 매 프레임마다 모든 그리기 명령이 전달되고, 레이어 분리 방식은 정적 요소의 그리기 명령은 한 번만 전달되고, 이후에는 비디오 프레임에 대한 명령만 전달된다.
4. SKIA의 렌더링 처리
4-1. CPU 처리 단계
단일 캔버스 방식은 매 프레임마다 모든 요소들의 픽셀을 새로 계산하고 레이어 분리 방식은 초기 한번만 정적 요소들의 픽셀을 계산하고 이후에는 비디오 픽셀 데이터만 계산하면 된다. 이후 GPU로 해당 데이터를 전송하는데, 데이터 전송에 많은 시간이 걸리기 때문에 이를 최소화하는 것이 중요하다.
4-2. GPU 처리 단계
단일 캔버스 방식에서는 매 프레임마다 모든 텍스처를 생성하고 모든 픽셀에 대해 복잡한 블렌딩 연산이 필요하지만, 레이어 분리 방식에서는 정적 텍스처는 GPU 메모리에 캐시 하여 간단한 레이어 합성 작업만 수행하게 된다.
사실 복잡하게 말했지만 안 그려도 되는 부분들을 매 프레임마다 새로 그려주기 때문에 그 부분을 최적화하기 위해 레이어를 분리하는 게 유리하다. 그런데 캔버스를 여러 개로 분리하게 되면 메모리 사용량도 그만큼 증가하는 트레이드오프가 발생한다. 어느 정도인지 대략적으로 계산해 보자. 1920x1080 해상도의 단일 캔버스는 RGBA 4byte를 곱하면 약 8.3MB의 메모리를 사용하고, GPU 텍스처에서도 같은 메모리가 사용되므로 약 16.6MB의 메모리를 사용하게 된다. 레이어 분리 방식은 웹캠과 화면공유, 상호작용, 이미지와 텍스트, 그리기의 4개의 캔버스를 사용하므로 4배인 약 66.4MB의 메모리를 사용한다. 이는 현대 하드웨어에서는 큰 문제가 되지 않는 반면, CPU 사용량이 높아지면 프레임드롭, 브라우저 전체 성능 저하, 사용자 입력에 대한 반응 지연 등 실제 방송에서 치명적인 문제들이 발생할 수 있기 때문에 CPU 최적화를 우선순위에 두어야 한다.
2. 여러 캔버스를 개별적으로 송신
그다음 생각한 방식은 각 요소를 독립적인 캔버스에 그리고 각각을 별도의 스트림으로 전송한 후 수신 측에서 합성하는 방식이다. 이 방식은 각 스트림별로 다른 해상도와 품질을 설정할 수 있고 동적인 레이아웃 변경이 용이하다. 하지만 서버에서 합성을 구현해야 하므로 무엇보다서버에서의 구현이 매우 복잡하고 서버 리소스가 강력해야 한다. 또한 높은 네트워크 대역폭도 필요한데, 대략적으로 계산해 보면 1920 x 1080 해상도에서 한 프레임의 크기는 1920 x 1080 x 4 = 약 8.3MB, 초당 30 프레임 전송 시 8.3MB x 30 = 약 249MB/s이다. 일반적인 코덱으로 데이터를 압축하면 약 1/100 정도로 압축할 수 있고 실제 전송되는 데이터는 2.49MB/s이다. 4개의 레이어를 전송해야 하므로 2.49MB/s x 4 = 약 10MB/s의 데이터를 전송해야 하고, 이는 약 80 Mbps이다. 이는 가정용 기가인터넷(1 Gbps)을 사용중일 때는 여유롭고, 5G(200-300 Mbps) 속도의 1/3, 4G LTE(50 Mbps) 속도의 1.6배 정도이다. 따라서 일반적인 인터넷 사용에는 충분한 속도이지만 그렇게 여유 있는 정도는 아니라고 볼 수 있다.
3. 여러 캔버스를 사용해 합성 후 스트림 송신
그래서 최종적으로 여러 캔버스를 합성하여 송신하는 방법을 채택했다. 이 방법은 프론트엔드에서의 구현이 복잡하고 추가적인 메모리 사용량이 있고 합성 시 추가적인 오버헤드가 발생한다. 하지만 개별 레이어 관리가 용이하고 네트워크 효율성을 유지할 수 있으며 정적인 콘텐츠를 재사용하고 GPU가 레이어 합성을 위해 최적화되어 있어 저장된 텍스처들을 효율적으로 합성할 수 있다.
전체 구조
초기 구조
최종적으로 이런 구조를 가지게 되었다.
각 설정 페이지마다 화면공유 스트림, 카메라/마이크 설정, 이미지 넣기, 텍스트와 펜, 지우개의 크기와 색상 등을 설정하고 여러 방송을 위한 상태들을 상태로 넘겨준다. 각 캔버스들의 동작은 각각 커스텀 훅으로 분리하고 실제 캔버스에 그려지는 이미지, 텍스트, 선들은 Context API로 관리했다. 활성화된 버튼에 따라 각 캔버스의 z-index를 변경하여 이벤트가 의도한 대로 동작하도록 했다. 방송 시작하기 버튼을 누르면 WHIP 연결 수립 후 캔버스들을 합성하여 스트림으로 전송하게 된다.
zustand 전역상태로 변경한 최종 구조
구현을 하면서 복잡성이 너무 증가하게 되어서 지역상태와 Context API로 관리되던 상태들을 Zustand 전역상태로 마이그레이션 했다. 무엇보다 매우 복잡했던 상태관리가 단일 스토어에서 관리되어 구현이 간단하고 가독성이 매우 매우 좋아졌다. 또한 선택적 구독을 통한 리렌더링 방지를 통해 성능을 최적화하고 각 캔버스 레이어의 상태를 독립적으로 업데이트할 수 있게 되었다.
Zustand store에 스트림 상태, paths, images, texts 등을 저장하고 useDraw, useImage, useText 커스텀 훅으로 관리한다. 스트림이 변경되거나 이미지/텍스트를 삽입하거나 그림이 그려지면 해당 정보를 각 훅에서 처리하여 해당 store를 업데이트한다. 각 캔버스의 useEffect에서 store가 업데이트되면 캔버스를 다시 그리는 식으로 동작한다. Interaction의 경우는 조금 특이한데, 캔버스 위의 어느 요소를 클릭하면 그 요소의 감싸는 테두리가 그 요소의 캔버스가 아니라 Interaction Canvas위로 생긴다. 해당 테두리를 드래그하거나 리사이즈 하면 store에 저장된 해당 요소의 위치 상태가 업데이트 되고 이에 따라 해당 캔버스에서도 다시 그리게된다.
참고
https://juneyr.dev/webrtc-basics
실시간 통화 어떻게 하는 거지 : WebRTC 기초
WebRTC 를 공부하기로한다. WebRTC가 뭔데 - 이름 그리고 비전 webRTC는 plugin-free web - Real Time Communication…
juneyr.dev
https://millo-l.github.io/WebRTC-%EC%9D%B4%EB%A1%A0-%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B0/
WebRTC 이론 정리하기 | millo's tech blog
WebRTC 이론 정리하기. ICE, SDP, STUN, TURN, NAT 등에 대해 알아보자.
millo-l.github.io
https://millo-l.github.io/WebRTC-%EA%B5%AC%ED%98%84-%EB%B0%A9%EC%8B%9D-Mesh-SFU-MCU/
WebRTC 구현 방식(Mesh/P2P, SFU, MCU) | millo's tech blog
WebRTC를 구현하기 위한 서버의 종류에 대해 알아보자. (Mesh/P2P, SFU, MCU)
millo-l.github.io
https://tv.naver.com/v/4578450
NAVER D2
책에서는 맛볼 수 없는 HTML5 Canvas 이야기
tv.naver.com
'프론트엔드' 카테고리의 다른 글
자바스크립트 깨알상식 (간단) (0) | 2025.01.26 |
---|---|
실행 컨텍스트와 클로저 이해해보기 (0) | 2025.01.26 |
V8 엔진 뜯어보기 (0) | 2025.01.21 |
브라우저에서 라이브 스트리밍을 송출해보자 - 웹 스튜디오 개발기 2 (개선 및 트러블슈팅) (0) | 2025.01.10 |
브라우저 내부 동작을 알아보자(+Web Worker) (2) | 2025.01.05 |